Skip to content

Commit

Permalink
Merge pull request #211 from duo-labs/fix/parse-reg-auth-options-json
Browse files Browse the repository at this point in the history
Add registration and authentication options JSON parsing
  • Loading branch information
MasterKale committed Mar 28, 2024
2 parents 7d73676 + 981e2f8 commit dc08bc0
Show file tree
Hide file tree
Showing 9 changed files with 1,009 additions and 2 deletions.
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

0 comments on commit dc08bc0

Please sign in to comment.