Skip to content

Commit dc08bc0

Browse files
authored
Merge pull request #211 from duo-labs/fix/parse-reg-auth-options-json
Add registration and authentication options JSON parsing
2 parents 7d73676 + 981e2f8 commit dc08bc0

9 files changed

+1009
-2
lines changed

.vscode/settings.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
"gitlens.advanced.blame.customArguments": [
1414
"--ignore-revs-file",
1515
".git-blame-ignore-revs"
16-
]
16+
],
17+
"python.analysis.autoImportCompletions": true
1718
}

README.md

+29
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,32 @@ Python's unittest module can be used to execute everything in the **tests/** dir
8585
```sh
8686
venv $> python -m unittest
8787
```
88+
89+
Auto-watching unittests can be achieved with a tool like nodemon.
90+
91+
**All tests:**
92+
```sh
93+
venv $> nodemon --exec "python -m unittest" --ext py
94+
```
95+
96+
**An individual test file:**
97+
```sh
98+
venv $> nodemon --exec "python -m unittest tests/test_aaguid_to_string.py" --ext py
99+
```
100+
101+
### Linting and Formatting
102+
103+
Linting is handled via `mypy`:
104+
105+
```sh
106+
venv $> python -m mypy webauthn
107+
Success: no issues found in 52 source files
108+
```
109+
110+
The entire library is formatted using `black`:
111+
112+
```sh
113+
venv $> python -m black webauthn --line-length=99
114+
All done! ✨ 🍰 ✨
115+
52 files left unchanged.
116+
```
+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
from email.mime import base
2+
from unittest import TestCase
3+
4+
from webauthn.helpers import base64url_to_bytes
5+
from webauthn.helpers.exceptions import InvalidJSONStructure
6+
from webauthn.helpers.structs import (
7+
AuthenticatorTransport,
8+
PublicKeyCredentialDescriptor,
9+
UserVerificationRequirement,
10+
)
11+
from webauthn.helpers.parse_authentication_options_json import parse_authentication_options_json
12+
13+
14+
class TestParseAuthenticationOptionsJSON(TestCase):
15+
maxDiff = None
16+
17+
def test_returns_parsed_options_simple(self) -> None:
18+
opts = parse_authentication_options_json(
19+
{
20+
"challenge": "skxyhJljbw-ZQn-g1i87FBWeJ8_8B78oihdtSmVYaI2mArvHxI7WyTEW3gIeIRamDPlh8PJOK-ThcQc3xPNYTQ",
21+
"timeout": 60000,
22+
"rpId": "example.com",
23+
"allowCredentials": [],
24+
"userVerification": "preferred",
25+
}
26+
)
27+
28+
self.assertEqual(
29+
opts.challenge,
30+
base64url_to_bytes(
31+
"skxyhJljbw-ZQn-g1i87FBWeJ8_8B78oihdtSmVYaI2mArvHxI7WyTEW3gIeIRamDPlh8PJOK-ThcQc3xPNYTQ"
32+
),
33+
)
34+
self.assertEqual(opts.timeout, 60000)
35+
self.assertEqual(opts.rp_id, "example.com")
36+
self.assertEqual(opts.allow_credentials, [])
37+
self.assertEqual(opts.user_verification, UserVerificationRequirement.PREFERRED)
38+
39+
def test_returns_parsed_options_full(self) -> None:
40+
opts = parse_authentication_options_json(
41+
{
42+
"challenge": "MTIzNDU2Nzg5MA",
43+
"timeout": 12000,
44+
"rpId": "example.com",
45+
"allowCredentials": [
46+
{
47+
"id": "MTIzNDU2Nzg5MA",
48+
"type": "public-key",
49+
"transports": ["internal", "hybrid"],
50+
}
51+
],
52+
"userVerification": "required",
53+
}
54+
)
55+
56+
self.assertEqual(opts.challenge, base64url_to_bytes("MTIzNDU2Nzg5MA"))
57+
self.assertEqual(opts.timeout, 12000)
58+
self.assertEqual(opts.rp_id, "example.com")
59+
self.assertEqual(
60+
opts.allow_credentials,
61+
[
62+
PublicKeyCredentialDescriptor(
63+
id=base64url_to_bytes("MTIzNDU2Nzg5MA"),
64+
transports=[AuthenticatorTransport.INTERNAL, AuthenticatorTransport.HYBRID],
65+
)
66+
],
67+
)
68+
self.assertEqual(opts.user_verification, UserVerificationRequirement.REQUIRED)
69+
70+
def test_supports_json_string(self) -> None:
71+
opts = parse_authentication_options_json(
72+
'{"challenge": "skxyhJljbw-ZQn-g1i87FBWeJ8_8B78oihdtSmVYaI2mArvHxI7WyTEW3gIeIRamDPlh8PJOK-ThcQc3xPNYTQ", "timeout": 60000, "rpId": "example.com", "allowCredentials": [], "userVerification": "preferred"}'
73+
)
74+
75+
self.assertEqual(
76+
opts.challenge,
77+
base64url_to_bytes(
78+
"skxyhJljbw-ZQn-g1i87FBWeJ8_8B78oihdtSmVYaI2mArvHxI7WyTEW3gIeIRamDPlh8PJOK-ThcQc3xPNYTQ"
79+
),
80+
)
81+
self.assertEqual(opts.timeout, 60000)
82+
self.assertEqual(opts.rp_id, "example.com")
83+
self.assertEqual(opts.allow_credentials, [])
84+
self.assertEqual(opts.user_verification, UserVerificationRequirement.PREFERRED)
85+
86+
def test_raises_on_non_dict_json(self) -> None:
87+
with self.assertRaisesRegex(InvalidJSONStructure, "not a JSON object"):
88+
parse_authentication_options_json("[0]")
89+
90+
def test_raises_on_missing_challenge(self) -> None:
91+
with self.assertRaisesRegex(InvalidJSONStructure, "missing required challenge"):
92+
parse_authentication_options_json({})
93+
94+
def test_supports_optional_timeout(self) -> None:
95+
opts = parse_authentication_options_json(
96+
{
97+
"challenge": "aaa",
98+
"userVerification": "required",
99+
}
100+
)
101+
102+
self.assertIsNone(opts.timeout)
103+
104+
def test_supports_optional_rp_id(self) -> None:
105+
opts = parse_authentication_options_json(
106+
{
107+
"challenge": "aaa",
108+
"userVerification": "required",
109+
}
110+
)
111+
112+
self.assertIsNone(opts.rp_id)
113+
114+
def test_raises_on_missing_user_verification(self) -> None:
115+
with self.assertRaisesRegex(InvalidJSONStructure, "missing required userVerification"):
116+
parse_authentication_options_json(
117+
{
118+
"challenge": "aaaa",
119+
}
120+
)
121+
122+
def test_raises_on_invalid_user_verification(self) -> None:
123+
with self.assertRaisesRegex(InvalidJSONStructure, "userVerification was invalid"):
124+
parse_authentication_options_json(
125+
{
126+
"challenge": "aaaa",
127+
"userVerification": "when_inconvenient",
128+
}
129+
)
130+
131+
def test_supports_optional_allow_credentials(self) -> None:
132+
opts = parse_authentication_options_json(
133+
{
134+
"challenge": "aaa",
135+
"userVerification": "required",
136+
}
137+
)
138+
139+
self.assertIsNone(opts.allow_credentials)
140+
141+
def test_raises_on_allow_credentials_entry_missing_id(self) -> None:
142+
with self.assertRaisesRegex(InvalidJSONStructure, "missing required id"):
143+
parse_authentication_options_json(
144+
{
145+
"challenge": "aaa",
146+
"userVerification": "required",
147+
"allowCredentials": [{}],
148+
}
149+
)
150+
151+
def test_raises_on_allow_credentials_entry_invalid_transports(self) -> None:
152+
with self.assertRaisesRegex(InvalidJSONStructure, "transports was not list"):
153+
parse_authentication_options_json(
154+
{
155+
"challenge": "aaa",
156+
"userVerification": "required",
157+
"allowCredentials": [{"id": "aaaa", "transports": ""}],
158+
}
159+
)
160+
161+
def test_raises_on_allow_credentials_entry_invalid_transports_entry(self) -> None:
162+
with self.assertRaisesRegex(InvalidJSONStructure, "entry transports had invalid value"):
163+
parse_authentication_options_json(
164+
{
165+
"challenge": "aaa",
166+
"userVerification": "required",
167+
"allowCredentials": [{"id": "aaaa", "transports": ["pcie"]}],
168+
}
169+
)

tests/test_parse_registration_credential_json.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from webauthn.helpers.parse_registration_credential_json import parse_registration_credential_json
77

88

9-
class TestParseClientDataJSON(TestCase):
9+
class TestParseRegistrationCredentialJSON(TestCase):
1010
def test_raises_on_non_dict_json(self) -> None:
1111
with self.assertRaisesRegex(InvalidJSONStructure, "not a JSON object"):
1212
parse_registration_credential_json("[0]")

0 commit comments

Comments
 (0)