Skip to content

Commit dbbd8fc

Browse files
committed
Adds typecast handling for passkey request handlers. instantiates gson at root level of the class to avoid multiple reinstantiation
1 parent 705c046 commit dbbd8fc

7 files changed

Lines changed: 213 additions & 25 deletions

File tree

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ jobs:
309309

310310
test-windows-unit:
311311
name: Run native Windows unit tests
312-
runs-on: windows-2022
312+
runs-on: windows-latest
313313
environment: ${{ github.event.pull_request.head.repo.fork && 'external' || 'internal' }}
314314

315315
steps:
@@ -349,7 +349,7 @@ jobs:
349349
${{ github.workspace }}\vcpkg\vcpkg install cpprestsdk:x64-windows openssl:x64-windows boost-system:x64-windows boost-date-time:x64-windows boost-regex:x64-windows
350350
shell: cmd
351351
env:
352-
VCPKG_BINARY_SOURCES: 'clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite'
352+
VCPKG_BINARY_SOURCES: 'clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite;x-gha,readwrite'
353353

354354
- name: Cache Windows example app build
355355
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # pin@v5.0.5

auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.auth0.android.result.RecoveryCodeEnrollmentChallenge
1313
import com.auth0.android.result.TotpAuthenticationMethod
1414
import com.auth0.android.result.TotpEnrollmentChallenge
1515
import com.google.gson.Gson
16+
import com.google.gson.reflect.TypeToken
1617

1718
fun AuthenticationMethod.toMyAccountMethodMap(): Map<String, Any?> {
1819
return buildMap {
@@ -67,16 +68,17 @@ fun PasskeyAuthenticationMethod.toMyAccountPasskeyMethodMap(): Map<String, Any?>
6768
}
6869

6970
fun PasskeyEnrollmentChallenge.toMyAccountPasskeyChallengeMap(): Map<String, Any?> {
70-
// Forward the full WebAuthn creation options so the create-credential step
71-
// can pass them to the platform authenticator verbatim.
72-
val authParamsPublicKey: Map<*, *> = Gson().fromJson(
73-
Gson().toJson(authParamsPublicKey),
74-
Map::class.java
71+
val gson = Gson()
72+
// AuthnParamsPublicKey is a typed data class; convert via JSON tree to avoid
73+
// a toJson()/fromJson(String) round-trip through an intermediate String.
74+
val authParamsPublicKeyMap: Map<String, Any> = gson.fromJson(
75+
gson.toJsonTree(authParamsPublicKey),
76+
object : TypeToken<Map<String, Any>>() {}.type
7577
)
7678
return buildMap {
7779
put("authenticationMethodId", authenticationMethodId)
7880
put("authSession", authSession)
79-
put("authParamsPublicKey", authParamsPublicKey)
81+
put("authParamsPublicKey", authParamsPublicKeyMap)
8082
}
8183
}
8284

auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeyCredentialExchangeApiRequestHandler.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,12 @@ class PasskeyCredentialExchangeApiRequestHandler : ApiRequestHandler {
3636

3737
assertHasProperties(listOf("challenge.authSession", "credential"), args)
3838

39-
val challenge = args["challenge"] as Map<*, *>
40-
val authSession = challenge["authSession"] as String
41-
val credentialMap = args["credential"] as Map<*, *>
39+
val challenge = args["challenge"] as? Map<*, *>
40+
?: throw IllegalArgumentException("Required property 'challenge' must be a map.")
41+
val authSession = challenge["authSession"] as? String
42+
?: throw IllegalArgumentException("Required property 'challenge.authSession' must be a string.")
43+
val credentialMap = args["credential"] as? Map<*, *>
44+
?: throw IllegalArgumentException("Required property 'credential' must be a map.")
4245

4346
val connection = args["connection"] as? String
4447
val organization = args["organization"] as? String

auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPasskeyRequestHandler.kt

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@ class EnrollPasskeyRequestHandler : MyAccountRequestHandler {
4444
request.data
4545
)
4646

47-
val challengeMap = request.data["challenge"] as Map<*, *>
48-
val credentialMap = request.data["credential"] as Map<*, *>
47+
val challengeMap = request.data["challenge"] as? Map<*, *>
48+
?: throw IllegalArgumentException("Required property 'challenge' must be a map.")
49+
val credentialMap = request.data["credential"] as? Map<*, *>
50+
?: throw IllegalArgumentException("Required property 'credential' must be a map.")
4951

5052
val challenge = reconstructChallenge(challengeMap)
5153
val credentials = reconstructCredentials(credentialMap)
@@ -83,16 +85,22 @@ class EnrollPasskeyRequestHandler : MyAccountRequestHandler {
8385
AuthnParamsPublicKey::class.java
8486
)
8587
return PasskeyEnrollmentChallenge(
86-
challengeMap["authenticationMethodId"] as String,
87-
challengeMap["authSession"] as String,
88+
challengeMap["authenticationMethodId"] as? String
89+
?: throw IllegalArgumentException(
90+
"Required property 'challenge.authenticationMethodId' must be a string."),
91+
challengeMap["authSession"] as? String
92+
?: throw IllegalArgumentException(
93+
"Required property 'challenge.authSession' must be a string."),
8894
authParamsPublicKey
8995
)
9096
}
9197

9298
private fun reconstructCredentials(
9399
credentialMap: Map<*, *>
94100
): PublicKeyCredentials {
95-
val response = credentialMap["response"] as Map<*, *>
101+
val response = credentialMap["response"] as? Map<*, *>
102+
?: throw IllegalArgumentException(
103+
"Required property 'credential.response' must be a map.")
96104
val clientExtensionResults =
97105
credentialMap["clientExtensionResults"] as? Map<*, *>
98106
val credProps = clientExtensionResults?.get("credProps") as? Map<*, *>
@@ -105,12 +113,20 @@ class EnrollPasskeyRequestHandler : MyAccountRequestHandler {
105113
clientExtensionResults = ClientExtensionResults(
106114
credProps = CredProps(rk = residentKey)
107115
),
108-
id = credentialMap["id"] as String,
109-
rawId = credentialMap["rawId"] as String,
116+
id = credentialMap["id"] as? String
117+
?: throw IllegalArgumentException(
118+
"Required property 'credential.id' must be a string."),
119+
rawId = credentialMap["rawId"] as? String
120+
?: throw IllegalArgumentException(
121+
"Required property 'credential.rawId' must be a string."),
110122
response = Response(
111-
attestationObject = response["attestationObject"] as String,
123+
attestationObject = response["attestationObject"] as? String
124+
?: throw IllegalArgumentException(
125+
"Required property 'credential.response.attestationObject' must be a string."),
112126
authenticatorData = (response["authenticatorData"] as? String) ?: "",
113-
clientDataJSON = response["clientDataJSON"] as String,
127+
clientDataJSON = response["clientDataJSON"] as? String
128+
?: throw IllegalArgumentException(
129+
"Required property 'credential.response.clientDataJSON' must be a string."),
114130
transports = (response["transports"] as? List<*>)
115131
?.filterIsInstance<String>() ?: emptyList(),
116132
signature = (response["signature"] as? String) ?: "",

auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeyCredentialExchangeApiRequestHandlerTest.kt

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,74 @@ class PasskeyCredentialExchangeApiRequestHandlerTest {
119119
)
120120
}
121121

122+
@Test
123+
fun `should throw when challenge is not a map`() {
124+
// assertHasProperties fires first because tryGetByKey returns null for a
125+
// non-Map; the resulting message still clearly identifies the bad field.
126+
val options = hashMapOf<String, Any>(
127+
"challenge" to "not-a-map",
128+
"credential" to loginCredentialMap()
129+
)
130+
val handler = PasskeyCredentialExchangeApiRequestHandler()
131+
val mockApi = mock<AuthenticationAPIClient>()
132+
val mockAccount = mock<Auth0>()
133+
val mockResult = mock<Result>()
134+
val request = MethodCallRequest(account = mockAccount, options)
135+
136+
val exception = Assert.assertThrows(IllegalArgumentException::class.java) {
137+
handler.handle(mockApi, request, mockResult)
138+
}
139+
140+
assertThat(
141+
exception.message,
142+
equalTo("Required property 'challenge.authSession' is not provided.")
143+
)
144+
}
145+
146+
@Test
147+
fun `should throw when authSession is not a string`() {
148+
val options = hashMapOf<String, Any>(
149+
"challenge" to mapOf("authSession" to 42),
150+
"credential" to loginCredentialMap()
151+
)
152+
val handler = PasskeyCredentialExchangeApiRequestHandler()
153+
val mockApi = mock<AuthenticationAPIClient>()
154+
val mockAccount = mock<Auth0>()
155+
val mockResult = mock<Result>()
156+
val request = MethodCallRequest(account = mockAccount, options)
157+
158+
val exception = Assert.assertThrows(IllegalArgumentException::class.java) {
159+
handler.handle(mockApi, request, mockResult)
160+
}
161+
162+
assertThat(
163+
exception.message,
164+
equalTo("Required property 'challenge.authSession' must be a string.")
165+
)
166+
}
167+
168+
@Test
169+
fun `should throw when credential is not a map`() {
170+
val options = hashMapOf<String, Any>(
171+
"challenge" to challengeMap(),
172+
"credential" to "not-a-map"
173+
)
174+
val handler = PasskeyCredentialExchangeApiRequestHandler()
175+
val mockApi = mock<AuthenticationAPIClient>()
176+
val mockAccount = mock<Auth0>()
177+
val mockResult = mock<Result>()
178+
val request = MethodCallRequest(account = mockAccount, options)
179+
180+
val exception = Assert.assertThrows(IllegalArgumentException::class.java) {
181+
handler.handle(mockApi, request, mockResult)
182+
}
183+
184+
assertThat(
185+
exception.message,
186+
equalTo("Required property 'credential' must be a map.")
187+
)
188+
}
189+
122190
@Test
123191
fun `should call signinWithPasskey and configure scope, audience and parameters`() {
124192
val options = hashMapOf<String, Any>(

auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPasskeyRequestHandlerTest.kt

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,110 @@ class EnrollPasskeyRequestHandlerTest {
139139
verify(mockResult).error(eq("server_error"), eq("Server error"), any())
140140
}
141141

142+
@Test
143+
fun `should throw when challenge is not a map`() {
144+
// assertHasProperties fires first because tryGetByKey returns null for a
145+
// non-Map value; the message still identifies the bad field.
146+
val handler = EnrollPasskeyRequestHandler()
147+
val mockResult = mock<Result>()
148+
val mockAccount = mock<Auth0>()
149+
val mockClient = mock<MyAccountAPIClient>()
150+
val request = MethodCallRequest(
151+
account = mockAccount,
152+
hashMapOf<String, Any>(
153+
"challenge" to "not-a-map",
154+
"credential" to credentialMap()
155+
)
156+
)
157+
158+
val exception = Assert.assertThrows(IllegalArgumentException::class.java) {
159+
handler.handle(mockClient, request, mockResult)
160+
}
161+
162+
assertThat(
163+
exception.message,
164+
equalTo("Required property 'challenge.authSession' is not provided.")
165+
)
166+
}
167+
168+
@Test
169+
fun `should throw when credential is not a map`() {
170+
// assertHasProperties fires first because tryGetByKey returns null for a
171+
// non-Map value; the message identifies the first missing nested field.
172+
val handler = EnrollPasskeyRequestHandler()
173+
val mockResult = mock<Result>()
174+
val mockAccount = mock<Auth0>()
175+
val mockClient = mock<MyAccountAPIClient>()
176+
val request = MethodCallRequest(
177+
account = mockAccount,
178+
hashMapOf<String, Any>(
179+
"challenge" to challengeMap(),
180+
"credential" to "not-a-map"
181+
)
182+
)
183+
184+
val exception = Assert.assertThrows(IllegalArgumentException::class.java) {
185+
handler.handle(mockClient, request, mockResult)
186+
}
187+
188+
assertThat(
189+
exception.message,
190+
equalTo("Required property 'credential.id' is not provided.")
191+
)
192+
}
193+
194+
@Test
195+
fun `should throw when authenticationMethodId is not a string`() {
196+
val handler = EnrollPasskeyRequestHandler()
197+
val mockResult = mock<Result>()
198+
val mockAccount = mock<Auth0>()
199+
val mockClient = mock<MyAccountAPIClient>()
200+
val challenge = challengeMap()
201+
challenge["authenticationMethodId"] = 42
202+
val request = MethodCallRequest(
203+
account = mockAccount,
204+
hashMapOf<String, Any>(
205+
"challenge" to challenge,
206+
"credential" to credentialMap()
207+
)
208+
)
209+
210+
val exception = Assert.assertThrows(IllegalArgumentException::class.java) {
211+
handler.handle(mockClient, request, mockResult)
212+
}
213+
214+
assertThat(
215+
exception.message,
216+
equalTo("Required property 'challenge.authenticationMethodId' must be a string.")
217+
)
218+
}
219+
220+
@Test
221+
fun `should throw when authSession is not a string`() {
222+
val handler = EnrollPasskeyRequestHandler()
223+
val mockResult = mock<Result>()
224+
val mockAccount = mock<Auth0>()
225+
val mockClient = mock<MyAccountAPIClient>()
226+
val challenge = challengeMap()
227+
challenge["authSession"] = 42
228+
val request = MethodCallRequest(
229+
account = mockAccount,
230+
hashMapOf<String, Any>(
231+
"challenge" to challenge,
232+
"credential" to credentialMap()
233+
)
234+
)
235+
236+
val exception = Assert.assertThrows(IllegalArgumentException::class.java) {
237+
handler.handle(mockClient, request, mockResult)
238+
}
239+
240+
assertThat(
241+
exception.message,
242+
equalTo("Required property 'challenge.authSession' must be a string.")
243+
)
244+
}
245+
142246
@Test(expected = IllegalArgumentException::class)
143247
fun `should throw when challenge is missing`() {
144248
val handler = EnrollPasskeyRequestHandler()

auth0_flutter/example/android/app/build.gradle

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,6 @@ flutter {
9494

9595
dependencies {
9696
kover(project(":auth0_flutter"))
97-
98-
// Credential Manager — used by the example's PasskeyAuthenticator to present
99-
// the OS passkey UI. The auth0_flutter SDK does not present this UI itself.
100-
implementation 'androidx.credentials:credentials:1.3.0'
101-
implementation 'androidx.credentials:credentials-play-services-auth:1.3.0'
10297
}
10398

10499
koverReport {

0 commit comments

Comments
 (0)