Skip to content

Commit d5d2597

Browse files
authored
Making realm parameter optional for passkeys (#776)
2 parents d58083d + b51aca2 commit d5d2597

File tree

6 files changed

+85
-41
lines changed

6 files changed

+85
-41
lines changed

auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
156156

157157
/**
158158
* Sign-in a user using passkeys.
159-
* This should be called after the client has received the passkey challenge and auth-session from the server
159+
* This should be called after the client has received the passkey challenge from the server and generated the public key response.
160160
* The default scope used is 'openid profile email'.
161161
*
162162
* Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types)
@@ -175,19 +175,19 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
175175
* ```
176176
*
177177
* @param authSession the auth session received from the server as part of the public key challenge request.
178-
* @param authResponse the public key credential authentication response
179-
* @param realm the default connection to use
178+
* @param authResponse the [PublicKeyCredentials] authentication response
179+
* @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant
180180
* @return a request to configure and start that will yield [Credentials]
181181
*/
182182
public fun signinWithPasskey(
183183
authSession: String,
184184
authResponse: PublicKeyCredentials,
185-
realm: String
185+
realm: String? = null
186186
): AuthenticationRequest {
187187
val params = ParameterBuilder.newBuilder().apply {
188188
setGrantType(ParameterBuilder.GRANT_TYPE_PASSKEY)
189189
set(AUTH_SESSION_KEY, authSession)
190-
setRealm(realm)
190+
realm?.let { setRealm(it) }
191191
}.asDictionary()
192192

193193
return loginWithToken(params)
@@ -198,6 +198,44 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
198198
}
199199

200200

201+
/**
202+
* Sign-in a user using passkeys.
203+
* This should be called after the client has received the passkey challenge from the server and generated the public key response.
204+
* The default scope used is 'openid profile email'.
205+
*
206+
* Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types)
207+
* to learn how to enable it.
208+
*
209+
* Example usage:
210+
*
211+
* ```
212+
* client.signinWithPasskey("{authSession}", "{authResponse}","{realm}")
213+
* .validateClaims() //mandatory
214+
* .addParameter("scope","scope")
215+
* .start(object: Callback<Credentials, AuthenticationException> {
216+
* override fun onFailure(error: AuthenticationException) { }
217+
* override fun onSuccess(result: Credentials) { }
218+
* })
219+
* ```
220+
*
221+
* @param authSession the auth session received from the server as part of the public key challenge request.
222+
* @param authResponse the public key credential authentication response in JSON string format that follows the standard webauthn json format
223+
* @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant
224+
* @return a request to configure and start that will yield [Credentials]
225+
*/
226+
public fun signinWithPasskey(
227+
authSession: String,
228+
authResponse: String,
229+
realm: String? = null
230+
): AuthenticationRequest {
231+
val publicKeyCredentials = gson.fromJson(
232+
authResponse,
233+
PublicKeyCredentials::class.java
234+
)
235+
return signinWithPasskey(authSession, publicKeyCredentials, realm)
236+
}
237+
238+
201239
/**
202240
* Sign-up a user and returns a challenge for private and public key generation.
203241
* The default scope used is 'openid profile email'.
@@ -217,22 +255,22 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
217255
* ```
218256
*
219257
* @param userData user information of the client
220-
* @param realm default connection to use
258+
* @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant
221259
* @return a request to configure and start that will yield [PasskeyRegistrationChallenge]
222260
*/
223261
public fun signupWithPasskey(
224262
userData: UserData,
225-
realm: String
263+
realm: String? = null
226264
): Request<PasskeyRegistrationChallenge, AuthenticationException> {
227-
val user = Gson().toJsonTree(userData)
265+
val user = gson.toJsonTree(userData)
228266
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
229267
.addPathSegment(PASSKEY_PATH)
230268
.addPathSegment(REGISTER_PATH)
231269
.build()
232270

233271
val params = ParameterBuilder.newBuilder().apply {
234272
setClientId(clientId)
235-
setRealm(realm)
273+
realm?.let { setRealm(it) }
236274
}.asDictionary()
237275

238276
val passkeyRegistrationChallengeAdapter: JsonAdapter<PasskeyRegistrationChallenge> =
@@ -261,11 +299,11 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
261299
* })
262300
* ```
263301
*
264-
* @param realm A default connection name
302+
* @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant
265303
* @return a request to configure and start that will yield [PasskeyChallenge]
266304
*/
267305
public fun passkeyChallenge(
268-
realm: String
306+
realm: String? = null
269307
): Request<PasskeyChallenge, AuthenticationException> {
270308
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
271309
.addPathSegment(PASSKEY_PATH)
@@ -274,7 +312,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
274312

275313
val parameters = ParameterBuilder.newBuilder().apply {
276314
setClientId(clientId)
277-
setRealm(realm)
315+
realm?.let { setRealm(it) }
278316
}.asDictionary()
279317

280318
val passkeyChallengeAdapter: JsonAdapter<PasskeyChallenge> = GsonAdapter(

auth0/src/main/java/com/auth0/android/provider/PasskeyManager.kt

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,6 @@ internal class PasskeyManager(
5454
callback: Callback<Credentials, AuthenticationException>,
5555
executor: Executor = Executors.newSingleThreadExecutor()
5656
) {
57-
58-
if (realm == null) {
59-
callback.onFailure(AuthenticationException("Realm is required for passkey authentication"))
60-
return
61-
}
6257
authenticationAPIClient.signupWithPasskey(userData, realm)
6358
.addParameters(parameters)
6459
.start(object : Callback<PasskeyRegistrationChallenge, AuthenticationException> {
@@ -120,10 +115,6 @@ internal class PasskeyManager(
120115
callback: Callback<Credentials, AuthenticationException>,
121116
executor: Executor = Executors.newSingleThreadExecutor()
122117
) {
123-
if (realm == null) {
124-
callback.onFailure(AuthenticationException("Realm is required for passkey authentication"))
125-
return
126-
}
127118
authenticationAPIClient.passkeyChallenge(realm)
128119
.start(object : Callback<PasskeyChallenge, AuthenticationException> {
129120
override fun onSuccess(result: PasskeyChallenge) {

auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.auth0.android.authentication.ParameterBuilder.Companion.newBuilder
77
import com.auth0.android.provider.JwtTestUtils
88
import com.auth0.android.request.HttpMethod
99
import com.auth0.android.request.NetworkingClient
10+
import com.auth0.android.request.PublicKeyCredentials
1011
import com.auth0.android.request.RequestOptions
1112
import com.auth0.android.request.ServerResponse
1213
import com.auth0.android.request.internal.RequestFactory
@@ -191,7 +192,7 @@ public class AuthenticationAPIClientTest {
191192
val callback = MockAuthenticationCallback<Credentials>()
192193
val auth0 = auth0
193194
val client = AuthenticationAPIClient(auth0)
194-
client.signinWithPasskey("auth-session", mock(), MY_CONNECTION)
195+
client.signinWithPasskey("auth-session", mock<PublicKeyCredentials>(), MY_CONNECTION)
195196
.start(callback)
196197
ShadowLooper.idleMainLooper()
197198
assertThat(

auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import com.auth0.android.authentication.AuthenticationException
1717
import com.auth0.android.authentication.request.AuthenticationRequestMock
1818
import com.auth0.android.authentication.request.RequestMock
1919
import com.auth0.android.callback.Callback
20+
import com.auth0.android.request.PublicKeyCredentials
2021
import com.auth0.android.request.UserData
2122
import com.auth0.android.result.AuthParamsPublicKey
2223
import com.auth0.android.result.AuthenticatorSelection
@@ -135,7 +136,13 @@ public class PasskeyManagerTest {
135136
`when`(authenticationAPIClient.signupWithPasskey(userMetadata, "testRealm")).thenReturn(
136137
RequestMock(passkeyRegistrationChallengeResponse, null)
137138
)
138-
`when`(authenticationAPIClient.signinWithPasskey(any(), any(), any())).thenReturn(
139+
`when`(
140+
authenticationAPIClient.signinWithPasskey(
141+
any(),
142+
any<PublicKeyCredentials>(),
143+
any()
144+
)
145+
).thenReturn(
139146
AuthenticationRequestMock(
140147
Credentials(
141148
"expectedIdToken",
@@ -178,7 +185,7 @@ public class PasskeyManagerTest {
178185

179186
verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm")
180187
verify(credentialManager).createCredentialAsync(eq(context), any(), any(), any(), any())
181-
verify(authenticationAPIClient).signinWithPasskey(any(), any(), any())
188+
verify(authenticationAPIClient).signinWithPasskey(any(), any<PublicKeyCredentials>(), any())
182189
verify(callback).onSuccess(credentialsCaptor.capture())
183190
Assert.assertEquals("codeAccess", credentialsCaptor.firstValue.accessToken)
184191
Assert.assertEquals("codeScope", credentialsCaptor.firstValue.scope)
@@ -205,7 +212,11 @@ public class PasskeyManagerTest {
205212
serialExecutor
206213
)
207214
verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm")
208-
verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any())
215+
verify(authenticationAPIClient, never()).signinWithPasskey(
216+
any(),
217+
any<PublicKeyCredentials>(),
218+
any()
219+
)
209220
verify(credentialManager, never()).createCredentialAsync(
210221
any(),
211222
any(),
@@ -251,7 +262,11 @@ public class PasskeyManagerTest {
251262
)
252263
verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm")
253264
verify(credentialManager).createCredentialAsync(eq(context), any(), any(), any(), any())
254-
verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any())
265+
verify(authenticationAPIClient, never()).signinWithPasskey(
266+
any(),
267+
any<PublicKeyCredentials>(),
268+
any()
269+
)
255270
verify(callback).onFailure(exceptionCaptor.capture())
256271
Assert.assertEquals(
257272
AuthenticationException::class.java,
@@ -277,7 +292,7 @@ public class PasskeyManagerTest {
277292
PublicKeyCredential(registrationResponseJSON)
278293
)
279294

280-
`when`(authenticationAPIClient.signinWithPasskey(any(), any(), any())).thenReturn(
295+
`when`(authenticationAPIClient.signinWithPasskey(any(), any<PublicKeyCredentials>(), any())).thenReturn(
281296
AuthenticationRequestMock(
282297
Credentials(
283298
"expectedIdToken",
@@ -309,7 +324,7 @@ public class PasskeyManagerTest {
309324
any(),
310325
any()
311326
)
312-
verify(authenticationAPIClient).signinWithPasskey(any(), any(), any())
327+
verify(authenticationAPIClient).signinWithPasskey(any(), any<PublicKeyCredentials>(), any())
313328
verify(callback).onSuccess(credentialsCaptor.capture())
314329
Assert.assertEquals("codeAccess", credentialsCaptor.firstValue.accessToken)
315330
Assert.assertEquals("codeScope", credentialsCaptor.firstValue.scope)
@@ -335,7 +350,7 @@ public class PasskeyManagerTest {
335350
any(),
336351
any()
337352
)
338-
verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any())
353+
verify(authenticationAPIClient, never()).signinWithPasskey(any(), any<PublicKeyCredentials>(), any())
339354
verify(callback).onFailure(error)
340355
}
341356

@@ -369,7 +384,7 @@ public class PasskeyManagerTest {
369384
any(),
370385
any()
371386
)
372-
verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any())
387+
verify(authenticationAPIClient, never()).signinWithPasskey(any(), any<PublicKeyCredentials>(), any())
373388
verify(callback).onFailure(exceptionCaptor.capture())
374389
Assert.assertEquals(
375390
AuthenticationException::class.java,

sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class DatabaseLoginFragment : Fragment() {
120120
}
121121

122122
binding.btSignupPasskey.setOnClickListener {
123-
passkeySignup()
123+
passkeySignup(binding.textEmail.text.toString())
124124
}
125125

126126
binding.btSignInPasskey.setOnClickListener {
@@ -129,7 +129,7 @@ class DatabaseLoginFragment : Fragment() {
129129

130130
binding.btSignupPasskeyAsync.setOnClickListener {
131131
launchAsync {
132-
passkeySignupAsync()
132+
passkeySignupAsync(binding.textEmail.text.toString())
133133
}
134134
}
135135

@@ -486,11 +486,11 @@ class DatabaseLoginFragment : Fragment() {
486486
}
487487
}
488488

489-
private fun passkeySignup() {
489+
private fun passkeySignup(email: String) {
490490
authenticationApiClient.signupWithPasskey(
491491
UserData(
492-
email = "jndoe@email.com"
493-
), "Username-Password-Authentication"
492+
email = email
493+
)
494494
).start(object : Callback<PasskeyRegistrationChallenge, AuthenticationException> {
495495
override fun onSuccess(result: PasskeyRegistrationChallenge) {
496496
val passKeyRegistrationChallenge = result
@@ -558,7 +558,7 @@ class DatabaseLoginFragment : Fragment() {
558558
}
559559

560560
private fun passkeySignin() {
561-
authenticationApiClient.passkeyChallenge("Username-Password-Authentication")
561+
authenticationApiClient.passkeyChallenge()
562562
.start(object : Callback<PasskeyChallenge, AuthenticationException> {
563563
override fun onSuccess(result: PasskeyChallenge) {
564564
val passkeyChallengeResponse = result
@@ -631,12 +631,11 @@ class DatabaseLoginFragment : Fragment() {
631631
})
632632
}
633633

634-
private suspend fun passkeySignupAsync() {
634+
private suspend fun passkeySignupAsync(email: String) {
635635

636636
try {
637637
val challenge = authenticationApiClient.signupWithPasskey(
638-
UserData(email = "[email protected]"),
639-
"Username-Password-Authentication"
638+
UserData(email = email)
640639
).await()
641640

642641
val request = CreatePublicKeyCredentialRequest(
@@ -682,7 +681,7 @@ class DatabaseLoginFragment : Fragment() {
682681
try {
683682

684683
val challenge =
685-
authenticationApiClient.passkeyChallenge("Username-Password-Authentication")
684+
authenticationApiClient.passkeyChallenge()
686685
.await()
687686

688687
val request = GetPublicKeyCredentialOption(Gson().toJson(challenge.authParamsPublicKey))
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<resources>
22
<string name="app_name">Auth0 SDK Sample</string>
3-
<string name="com_auth0_domain">pmathew.acmetest.org</string>
3+
<string name="com_auth0_domain">mathewp.acmetest.org</string>
44
<string name="com_auth0_client_id">gkba7X6OJM2b0cdlUlTCqXD7AwT3FYVV</string>
55
<string name="com_auth0_scheme">demo</string>
66
</resources>

0 commit comments

Comments
 (0)