Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add registration and authentication options JSON parsing #211

Merged
merged 12 commits into from
Mar 28, 2024
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
"gitlens.advanced.blame.customArguments": [
"--ignore-revs-file",
".git-blame-ignore-revs"
]
],
"python.analysis.autoImportCompletions": true
}
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,32 @@ Python's unittest module can be used to execute everything in the **tests/** dir
```sh
venv $> python -m unittest
```

Auto-watching unittests can be achieved with a tool like nodemon.

**All tests:**
```sh
venv $> nodemon --exec "python -m unittest" --ext py
```

**An individual test file:**
```sh
venv $> nodemon --exec "python -m unittest tests/test_aaguid_to_string.py" --ext py
```

### Linting and Formatting

Linting is handled via `mypy`:

```sh
venv $> python -m mypy webauthn
Success: no issues found in 52 source files
```

The entire library is formatted using `black`:

```sh
venv $> python -m black webauthn --line-length=99
All done! ✨ 🍰 ✨
52 files left unchanged.
```
169 changes: 169 additions & 0 deletions tests/test_parse_authentication_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
from email.mime import base
from unittest import TestCase

from webauthn.helpers import base64url_to_bytes
from webauthn.helpers.exceptions import InvalidJSONStructure
from webauthn.helpers.structs import (
AuthenticatorTransport,
PublicKeyCredentialDescriptor,
UserVerificationRequirement,
)
from webauthn.helpers.parse_authentication_options_json import parse_authentication_options_json


class TestParseAuthenticationOptionsJSON(TestCase):
maxDiff = None

def test_returns_parsed_options_simple(self) -> None:
opts = parse_authentication_options_json(
{
"challenge": "skxyhJljbw-ZQn-g1i87FBWeJ8_8B78oihdtSmVYaI2mArvHxI7WyTEW3gIeIRamDPlh8PJOK-ThcQc3xPNYTQ",
"timeout": 60000,
"rpId": "example.com",
"allowCredentials": [],
"userVerification": "preferred",
}
)

self.assertEqual(
opts.challenge,
base64url_to_bytes(
"skxyhJljbw-ZQn-g1i87FBWeJ8_8B78oihdtSmVYaI2mArvHxI7WyTEW3gIeIRamDPlh8PJOK-ThcQc3xPNYTQ"
),
)
self.assertEqual(opts.timeout, 60000)
self.assertEqual(opts.rp_id, "example.com")
self.assertEqual(opts.allow_credentials, [])
self.assertEqual(opts.user_verification, UserVerificationRequirement.PREFERRED)

def test_returns_parsed_options_full(self) -> None:
opts = parse_authentication_options_json(
{
"challenge": "MTIzNDU2Nzg5MA",
"timeout": 12000,
"rpId": "example.com",
"allowCredentials": [
{
"id": "MTIzNDU2Nzg5MA",
"type": "public-key",
"transports": ["internal", "hybrid"],
}
],
"userVerification": "required",
}
)

self.assertEqual(opts.challenge, base64url_to_bytes("MTIzNDU2Nzg5MA"))
self.assertEqual(opts.timeout, 12000)
self.assertEqual(opts.rp_id, "example.com")
self.assertEqual(
opts.allow_credentials,
[
PublicKeyCredentialDescriptor(
id=base64url_to_bytes("MTIzNDU2Nzg5MA"),
transports=[AuthenticatorTransport.INTERNAL, AuthenticatorTransport.HYBRID],
)
],
)
self.assertEqual(opts.user_verification, UserVerificationRequirement.REQUIRED)

def test_supports_json_string(self) -> None:
opts = parse_authentication_options_json(
'{"challenge": "skxyhJljbw-ZQn-g1i87FBWeJ8_8B78oihdtSmVYaI2mArvHxI7WyTEW3gIeIRamDPlh8PJOK-ThcQc3xPNYTQ", "timeout": 60000, "rpId": "example.com", "allowCredentials": [], "userVerification": "preferred"}'
)

self.assertEqual(
opts.challenge,
base64url_to_bytes(
"skxyhJljbw-ZQn-g1i87FBWeJ8_8B78oihdtSmVYaI2mArvHxI7WyTEW3gIeIRamDPlh8PJOK-ThcQc3xPNYTQ"
),
)
self.assertEqual(opts.timeout, 60000)
self.assertEqual(opts.rp_id, "example.com")
self.assertEqual(opts.allow_credentials, [])
self.assertEqual(opts.user_verification, UserVerificationRequirement.PREFERRED)

def test_raises_on_non_dict_json(self) -> None:
with self.assertRaisesRegex(InvalidJSONStructure, "not a JSON object"):
parse_authentication_options_json("[0]")

def test_raises_on_missing_challenge(self) -> None:
with self.assertRaisesRegex(InvalidJSONStructure, "missing required challenge"):
parse_authentication_options_json({})

def test_supports_optional_timeout(self) -> None:
opts = parse_authentication_options_json(
{
"challenge": "aaa",
"userVerification": "required",
}
)

self.assertIsNone(opts.timeout)

def test_supports_optional_rp_id(self) -> None:
opts = parse_authentication_options_json(
{
"challenge": "aaa",
"userVerification": "required",
}
)

self.assertIsNone(opts.rp_id)

def test_raises_on_missing_user_verification(self) -> None:
with self.assertRaisesRegex(InvalidJSONStructure, "missing required userVerification"):
parse_authentication_options_json(
{
"challenge": "aaaa",
}
)

def test_raises_on_invalid_user_verification(self) -> None:
with self.assertRaisesRegex(InvalidJSONStructure, "userVerification was invalid"):
parse_authentication_options_json(
{
"challenge": "aaaa",
"userVerification": "when_inconvenient",
}
)

def test_supports_optional_allow_credentials(self) -> None:
opts = parse_authentication_options_json(
{
"challenge": "aaa",
"userVerification": "required",
}
)

self.assertIsNone(opts.allow_credentials)

def test_raises_on_allow_credentials_entry_missing_id(self) -> None:
with self.assertRaisesRegex(InvalidJSONStructure, "missing required id"):
parse_authentication_options_json(
{
"challenge": "aaa",
"userVerification": "required",
"allowCredentials": [{}],
}
)

def test_raises_on_allow_credentials_entry_invalid_transports(self) -> None:
with self.assertRaisesRegex(InvalidJSONStructure, "transports was not list"):
parse_authentication_options_json(
{
"challenge": "aaa",
"userVerification": "required",
"allowCredentials": [{"id": "aaaa", "transports": ""}],
}
)

def test_raises_on_allow_credentials_entry_invalid_transports_entry(self) -> None:
with self.assertRaisesRegex(InvalidJSONStructure, "entry transports had invalid value"):
parse_authentication_options_json(
{
"challenge": "aaa",
"userVerification": "required",
"allowCredentials": [{"id": "aaaa", "transports": ["pcie"]}],
}
)
2 changes: 1 addition & 1 deletion tests/test_parse_registration_credential_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from webauthn.helpers.parse_registration_credential_json import parse_registration_credential_json


class TestParseClientDataJSON(TestCase):
class TestParseRegistrationCredentialJSON(TestCase):
def test_raises_on_non_dict_json(self) -> None:
with self.assertRaisesRegex(InvalidJSONStructure, "not a JSON object"):
parse_registration_credential_json("[0]")
Expand Down
Loading
Loading