From 85f97189afd0ba50cebb0ab3550771c1bab7e903 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Thu, 14 Nov 2024 10:52:23 +0530 Subject: [PATCH 1/2] Made the realm parameter optional in passkey --- .../authentication/AuthenticationAPIClient.kt | 18 +++++++++--------- .../auth0/android/provider/PasskeyManager.kt | 9 --------- .../com/auth0/sample/DatabaseLoginFragment.kt | 19 +++++++++---------- 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt index 05d36ddd..7c36de3d 100755 --- a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt @@ -176,18 +176,18 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe * * @param authSession the auth session received from the server as part of the public key challenge request. * @param authResponse the public key credential authentication response - * @param realm the default connection to use + * @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant * @return a request to configure and start that will yield [Credentials] */ public fun signinWithPasskey( authSession: String, authResponse: PublicKeyCredentials, - realm: String + realm: String? = null ): AuthenticationRequest { val params = ParameterBuilder.newBuilder().apply { setGrantType(ParameterBuilder.GRANT_TYPE_PASSKEY) set(AUTH_SESSION_KEY, authSession) - setRealm(realm) + realm?.let { setRealm(it) } }.asDictionary() return loginWithToken(params) @@ -217,12 +217,12 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe * ``` * * @param userData user information of the client - * @param realm default connection to use + * @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant * @return a request to configure and start that will yield [PasskeyRegistrationChallenge] */ public fun signupWithPasskey( userData: UserData, - realm: String + realm: String? = null ): Request { val user = Gson().toJsonTree(userData) val url = auth0.getDomainUrl().toHttpUrl().newBuilder() @@ -232,7 +232,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe val params = ParameterBuilder.newBuilder().apply { setClientId(clientId) - setRealm(realm) + realm?.let { setRealm(it) } }.asDictionary() val passkeyRegistrationChallengeAdapter: JsonAdapter = @@ -261,11 +261,11 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe * }) * ``` * - * @param realm A default connection name + * @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant * @return a request to configure and start that will yield [PasskeyChallenge] */ public fun passkeyChallenge( - realm: String + realm: String? = null ): Request { val url = auth0.getDomainUrl().toHttpUrl().newBuilder() .addPathSegment(PASSKEY_PATH) @@ -274,7 +274,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe val parameters = ParameterBuilder.newBuilder().apply { setClientId(clientId) - setRealm(realm) + realm?.let { setRealm(it) } }.asDictionary() val passkeyChallengeAdapter: JsonAdapter = GsonAdapter( diff --git a/auth0/src/main/java/com/auth0/android/provider/PasskeyManager.kt b/auth0/src/main/java/com/auth0/android/provider/PasskeyManager.kt index 6c5fd7b8..a6fc2fe9 100644 --- a/auth0/src/main/java/com/auth0/android/provider/PasskeyManager.kt +++ b/auth0/src/main/java/com/auth0/android/provider/PasskeyManager.kt @@ -54,11 +54,6 @@ internal class PasskeyManager( callback: Callback, executor: Executor = Executors.newSingleThreadExecutor() ) { - - if (realm == null) { - callback.onFailure(AuthenticationException("Realm is required for passkey authentication")) - return - } authenticationAPIClient.signupWithPasskey(userData, realm) .addParameters(parameters) .start(object : Callback { @@ -120,10 +115,6 @@ internal class PasskeyManager( callback: Callback, executor: Executor = Executors.newSingleThreadExecutor() ) { - if (realm == null) { - callback.onFailure(AuthenticationException("Realm is required for passkey authentication")) - return - } authenticationAPIClient.passkeyChallenge(realm) .start(object : Callback { override fun onSuccess(result: PasskeyChallenge) { diff --git a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt index 67e01b24..91e8f17f 100644 --- a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt +++ b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt @@ -120,7 +120,7 @@ class DatabaseLoginFragment : Fragment() { } binding.btSignupPasskey.setOnClickListener { - passkeySignup() + passkeySignup(binding.textEmail.text.toString()) } binding.btSignInPasskey.setOnClickListener { @@ -129,7 +129,7 @@ class DatabaseLoginFragment : Fragment() { binding.btSignupPasskeyAsync.setOnClickListener { launchAsync { - passkeySignupAsync() + passkeySignupAsync(binding.textEmail.text.toString()) } } @@ -486,11 +486,11 @@ class DatabaseLoginFragment : Fragment() { } } - private fun passkeySignup() { + private fun passkeySignup(email: String) { authenticationApiClient.signupWithPasskey( UserData( - email = "jndoe@email.com" - ), "Username-Password-Authentication" + email = email + ) ).start(object : Callback { override fun onSuccess(result: PasskeyRegistrationChallenge) { val passKeyRegistrationChallenge = result @@ -558,7 +558,7 @@ class DatabaseLoginFragment : Fragment() { } private fun passkeySignin() { - authenticationApiClient.passkeyChallenge("Username-Password-Authentication") + authenticationApiClient.passkeyChallenge() .start(object : Callback { override fun onSuccess(result: PasskeyChallenge) { val passkeyChallengeResponse = result @@ -631,12 +631,11 @@ class DatabaseLoginFragment : Fragment() { }) } - private suspend fun passkeySignupAsync() { + private suspend fun passkeySignupAsync(email: String) { try { val challenge = authenticationApiClient.signupWithPasskey( - UserData(email = "jdoe@email.com"), - "Username-Password-Authentication" + UserData(email = email) ).await() val request = CreatePublicKeyCredentialRequest( @@ -682,7 +681,7 @@ class DatabaseLoginFragment : Fragment() { try { val challenge = - authenticationApiClient.passkeyChallenge("Username-Password-Authentication") + authenticationApiClient.passkeyChallenge() .await() val request = GetPublicKeyCredentialOption(Gson().toJson(challenge.authParamsPublicKey)) From b51aca28d09de4a3a954da87a41d07ce242cef95 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Thu, 14 Nov 2024 14:17:46 +0530 Subject: [PATCH 2/2] Added overloaded siginPasskey method which takes the public credential json string --- .../authentication/AuthenticationAPIClient.kt | 44 +++++++++++++++++-- .../AuthenticationAPIClientTest.kt | 3 +- .../android/provider/PasskeyManagerTest.kt | 31 +++++++++---- sample/src/main/res/values/strings.xml | 2 +- 4 files changed, 67 insertions(+), 13 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt index 7c36de3d..18b25502 100755 --- a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt @@ -156,7 +156,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe /** * Sign-in a user using passkeys. - * This should be called after the client has received the passkey challenge and auth-session from the server + * This should be called after the client has received the passkey challenge from the server and generated the public key response. * The default scope used is 'openid profile email'. * * Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types) @@ -175,7 +175,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe * ``` * * @param authSession the auth session received from the server as part of the public key challenge request. - * @param authResponse the public key credential authentication response + * @param authResponse the [PublicKeyCredentials] authentication response * @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant * @return a request to configure and start that will yield [Credentials] */ @@ -198,6 +198,44 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe } + /** + * Sign-in a user using passkeys. + * This should be called after the client has received the passkey challenge from the server and generated the public key response. + * The default scope used is 'openid profile email'. + * + * Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types) + * to learn how to enable it. + * + * Example usage: + * + * ``` + * client.signinWithPasskey("{authSession}", "{authResponse}","{realm}") + * .validateClaims() //mandatory + * .addParameter("scope","scope") + * .start(object: Callback { + * override fun onFailure(error: AuthenticationException) { } + * override fun onSuccess(result: Credentials) { } + * }) + * ``` + * + * @param authSession the auth session received from the server as part of the public key challenge request. + * @param authResponse the public key credential authentication response in JSON string format that follows the standard webauthn json format + * @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant + * @return a request to configure and start that will yield [Credentials] + */ + public fun signinWithPasskey( + authSession: String, + authResponse: String, + realm: String? = null + ): AuthenticationRequest { + val publicKeyCredentials = gson.fromJson( + authResponse, + PublicKeyCredentials::class.java + ) + return signinWithPasskey(authSession, publicKeyCredentials, realm) + } + + /** * Sign-up a user and returns a challenge for private and public key generation. * The default scope used is 'openid profile email'. @@ -224,7 +262,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe userData: UserData, realm: String? = null ): Request { - val user = Gson().toJsonTree(userData) + val user = gson.toJsonTree(userData) val url = auth0.getDomainUrl().toHttpUrl().newBuilder() .addPathSegment(PASSKEY_PATH) .addPathSegment(REGISTER_PATH) diff --git a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt index 0bb0c77d..715b0b6f 100755 --- a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt @@ -7,6 +7,7 @@ import com.auth0.android.authentication.ParameterBuilder.Companion.newBuilder import com.auth0.android.provider.JwtTestUtils import com.auth0.android.request.HttpMethod import com.auth0.android.request.NetworkingClient +import com.auth0.android.request.PublicKeyCredentials import com.auth0.android.request.RequestOptions import com.auth0.android.request.ServerResponse import com.auth0.android.request.internal.RequestFactory @@ -191,7 +192,7 @@ public class AuthenticationAPIClientTest { val callback = MockAuthenticationCallback() val auth0 = auth0 val client = AuthenticationAPIClient(auth0) - client.signinWithPasskey("auth-session", mock(), MY_CONNECTION) + client.signinWithPasskey("auth-session", mock(), MY_CONNECTION) .start(callback) ShadowLooper.idleMainLooper() assertThat( diff --git a/auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt b/auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt index 72cf4814..4c39dea3 100644 --- a/auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt @@ -17,6 +17,7 @@ import com.auth0.android.authentication.AuthenticationException import com.auth0.android.authentication.request.AuthenticationRequestMock import com.auth0.android.authentication.request.RequestMock import com.auth0.android.callback.Callback +import com.auth0.android.request.PublicKeyCredentials import com.auth0.android.request.UserData import com.auth0.android.result.AuthParamsPublicKey import com.auth0.android.result.AuthenticatorSelection @@ -135,7 +136,13 @@ public class PasskeyManagerTest { `when`(authenticationAPIClient.signupWithPasskey(userMetadata, "testRealm")).thenReturn( RequestMock(passkeyRegistrationChallengeResponse, null) ) - `when`(authenticationAPIClient.signinWithPasskey(any(), any(), any())).thenReturn( + `when`( + authenticationAPIClient.signinWithPasskey( + any(), + any(), + any() + ) + ).thenReturn( AuthenticationRequestMock( Credentials( "expectedIdToken", @@ -178,7 +185,7 @@ public class PasskeyManagerTest { verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm") verify(credentialManager).createCredentialAsync(eq(context), any(), any(), any(), any()) - verify(authenticationAPIClient).signinWithPasskey(any(), any(), any()) + verify(authenticationAPIClient).signinWithPasskey(any(), any(), any()) verify(callback).onSuccess(credentialsCaptor.capture()) Assert.assertEquals("codeAccess", credentialsCaptor.firstValue.accessToken) Assert.assertEquals("codeScope", credentialsCaptor.firstValue.scope) @@ -205,7 +212,11 @@ public class PasskeyManagerTest { serialExecutor ) verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm") - verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any()) + verify(authenticationAPIClient, never()).signinWithPasskey( + any(), + any(), + any() + ) verify(credentialManager, never()).createCredentialAsync( any(), any(), @@ -251,7 +262,11 @@ public class PasskeyManagerTest { ) verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm") verify(credentialManager).createCredentialAsync(eq(context), any(), any(), any(), any()) - verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any()) + verify(authenticationAPIClient, never()).signinWithPasskey( + any(), + any(), + any() + ) verify(callback).onFailure(exceptionCaptor.capture()) Assert.assertEquals( AuthenticationException::class.java, @@ -277,7 +292,7 @@ public class PasskeyManagerTest { PublicKeyCredential(registrationResponseJSON) ) - `when`(authenticationAPIClient.signinWithPasskey(any(), any(), any())).thenReturn( + `when`(authenticationAPIClient.signinWithPasskey(any(), any(), any())).thenReturn( AuthenticationRequestMock( Credentials( "expectedIdToken", @@ -309,7 +324,7 @@ public class PasskeyManagerTest { any(), any() ) - verify(authenticationAPIClient).signinWithPasskey(any(), any(), any()) + verify(authenticationAPIClient).signinWithPasskey(any(), any(), any()) verify(callback).onSuccess(credentialsCaptor.capture()) Assert.assertEquals("codeAccess", credentialsCaptor.firstValue.accessToken) Assert.assertEquals("codeScope", credentialsCaptor.firstValue.scope) @@ -335,7 +350,7 @@ public class PasskeyManagerTest { any(), any() ) - verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any()) + verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any()) verify(callback).onFailure(error) } @@ -369,7 +384,7 @@ public class PasskeyManagerTest { any(), any() ) - verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any()) + verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any()) verify(callback).onFailure(exceptionCaptor.capture()) Assert.assertEquals( AuthenticationException::class.java, diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index 356e9c51..b87a506a 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ Auth0 SDK Sample - pmathew.acmetest.org + mathewp.acmetest.org gkba7X6OJM2b0cdlUlTCqXD7AwT3FYVV demo \ No newline at end of file