diff --git a/.snyk b/.snyk index 2c0f940d..08c5be66 100644 --- a/.snyk +++ b/.snyk @@ -5,16 +5,16 @@ ignore: SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135: - '*': reason: Latest version of dokka has this vulnerability - expires: 2024-10-31T12:19:35.000Z + expires: 2024-12-31T12:54:23.000Z created: 2024-08-01T12:08:37.770Z SNYK-JAVA-ORGJETBRAINSKOTLIN-2393744: - '*': reason: Latest version of dokka has this vulnerability - expires: 2024-10-31T12:19:35.000Z + expires: 2024-12-31T12:54:23.000Z created: 2024-08-01T12:08:55.927Z SNYK-JAVA-COMFASTERXMLJACKSONCORE-7569538: - '*': reason: Latest version of dokka has this vulnerability - expires: 2024-10-31T12:19:35.000Z + expires: 2024-12-31T1:54:23.000Z created: 2024-08-01T12:08:02.973Z patch: {} diff --git a/EXAMPLES.md b/EXAMPLES.md index 0ca1b74d..f2807055 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -610,61 +610,155 @@ User should have a custom domain configured and passkey grant-type enabled in th To sign up a user with passkey ```kotlin -PasskeyAuthProvider.signUp(account) - .setEmail("user email") - .setUserName("user name") - .setPhoneNumber("phone number") - .setRealm("optional connection name") - .start(object: Callback { - override fun onFailure(exception: AuthenticationException) { } - - override fun onSuccess(credentials: Credentials) { } - }) +// Using Coroutines +try { + val challenge = authenticationApiClient.signupWithPasskey( + "{user-data}", + "{realm}" + ).await() + + //Use CredentialManager to create public key credentials + val request = CreatePublicKeyCredentialRequest( + Gson().toJson(challenge.authParamsPublicKey) + ) + + val result = credentialManager.createCredential(requireContext(), request) + + val authRequest = Gson().fromJson( + (result as CreatePublicKeyCredentialResponse).registrationResponseJson, + PublicKeyCredentials::class.java + ) + + val userCredential = authenticationApiClient.signinWithPasskey( + challenge.authSession, authRequest, "{realm}" + ) + .validateClaims() + .await() +} catch (e: CreateCredentialException) { +} catch (exception: AuthenticationException) { +} ```
Using Java ```java -PasskeyAuthProvider authProvider = new PasskeyAuthProvider(); -authProvider.signUp(account) - .setEmail("user email") - .setUserName("user name") - .setPhoneNumber("phone number") - .setRealm("optional connection name") - .start(new Callback() { - @Override - public void onFailure(@NonNull AuthenticationException exception) { } - - @Override - public void onSuccess(@Nullable Credentials credentials) { } - }); + authenticationAPIClient.signupWithPasskey("{user-data}", "{realm}") + .start(new Callback() { + @Override + public void onSuccess(PasskeyRegistrationChallenge result) { + CreateCredentialRequest request = + new CreatePublicKeyCredentialRequest(new Gson().toJson(result.getAuthParamsPublicKey())); + credentialManager.createCredentialAsync(getContext(), + request, + cancellationSignal, + , + new CredentialManagerCallback() { + @Override + public void onResult(CreateCredentialResponse createCredentialResponse) { + PublicKeyCredentials credentials = new Gson().fromJson( + ((CreatePublicKeyCredentialResponse) createCredentialResponse).getRegistrationResponseJson(), + PublicKeyCredentials.class); + + authenticationAPIClient.signinWithPasskey(result.getAuthSession(), + credentials, "{realm}") + .start(new Callback() { + @Override + public void onSuccess(Credentials result) {} + + @Override + public void onFailure(@NonNull AuthenticationException error) {} + }); + } + @Override + public void onError(@NonNull CreateCredentialException e) {} + }); + } + + @Override + public void onFailure(@NonNull AuthenticationException error) {} +}); ```
To sign in a user with passkey ```kotlin -PasskeyAuthProvider.signin(account) - .setRealm("Optional connection name") - .start(object: Callback { - override fun onFailure(exception: AuthenticationException) { } +//Using coroutines +try { - override fun onSuccess(credentials: Credentials) { } - }) + val challenge = + authenticationApiClient.passkeyChallenge("{realm}") + .await() + + //Use CredentialManager to create public key credentials + val request = GetPublicKeyCredentialOption(Gson().toJson(challenge.authParamsPublicKey)) + val getCredRequest = GetCredentialRequest( + listOf(request) + ) + val result = credentialManager.getCredential(requireContext(), getCredRequest) + when (val credential = result.credential) { + is PublicKeyCredential -> { + val authRequest = Gson().fromJson( + credential.authenticationResponseJson, + PublicKeyCredentials::class.java + ) + val userCredential = authenticationApiClient.signinWithPasskey( + challenge.authSession, + authRequest, + "{realm}" + ) + .validateClaims() + .await() + } + + else -> {} + } +} catch (e: GetCredentialException) { +} catch (exception: AuthenticationException) { +} ```
Using Java ```java -PasskeyAuthProvider authProvider = new PasskeyAuthProvider(); -authProvider.signin(account) - .setRealm("optional connection name") - .start(new Callback() { - @Override - public void onFailure(@NonNull AuthenticationException exception) { } - - @Override - public void onSuccess(@Nullable Credentials credentials) { } - }); +authenticationAPIClient.passkeyChallenge("realm") + .start(new Callback() { + @Override + public void onSuccess(PasskeyChallenge result) { + GetPublicKeyCredentialOption option = new GetPublicKeyCredentialOption(new Gson().toJson(result.getAuthParamsPublicKey())); + GetCredentialRequest request = new GetCredentialRequest(List.of(option)); + credentialManager.getCredentialAsync(getContext(), + request, + cancellationSignal, + , + new CredentialManagerCallback() { + @Override + public void onResult(GetCredentialResponse getCredentialResponse) { + Credential credential = getCredentialResponse.getCredential(); + if (credential instanceof PublicKeyCredential) { + String responseJson = ((PublicKeyCredential) credential).getAuthenticationResponseJson(); + PublicKeyCredentials publicKeyCredentials = new Gson().fromJson( + responseJson, + PublicKeyCredentials.class + ); + authenticationAPIClient.signinWithPasskey(result.getAuthSession(), publicKeyCredentials,"{realm}") + .start(new Callback() { + @Override + public void onSuccess(Credentials result) {} + + @Override + public void onFailure(@NonNull AuthenticationException error) {} + }); + } + } + + @Override + public void onError(@NonNull GetCredentialException e) {} + }); + } + + @Override + public void onFailure(@NonNull AuthenticationException error) {} +}); ```
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 bf72e201..05d36ddd 100755 --- a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt @@ -12,8 +12,8 @@ import com.auth0.android.request.internal.ResponseUtils.isNetworkError import com.auth0.android.result.Challenge import com.auth0.android.result.Credentials import com.auth0.android.result.DatabaseUser -import com.auth0.android.result.PasskeyChallengeResponse -import com.auth0.android.result.PasskeyRegistrationResponse +import com.auth0.android.result.PasskeyChallenge +import com.auth0.android.result.PasskeyRegistrationChallenge import com.auth0.android.result.UserProfile import com.google.gson.Gson import okhttp3.HttpUrl.Companion.toHttpUrl @@ -155,25 +155,39 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe /** - * Log in a user using passkeys. - * This should be called after the client has received the Passkey challenge and Auth-session from the server . + * Sign-in a user using passkeys. + * This should be called after the client has received the passkey challenge and auth-session from the server + * 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. * - * @param authSession the auth session received from the server as part of the public challenge request. - * @param authResponse the public key credential response to be sent to the server - * @param parameters additional parameters to be sent as part of the request + * 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 + * @param realm the default connection to use * @return a request to configure and start that will yield [Credentials] */ - internal fun signinWithPasskey( + public fun signinWithPasskey( authSession: String, - authResponse: PublicKeyCredentialResponse, - parameters: Map + authResponse: PublicKeyCredentials, + realm: String ): AuthenticationRequest { val params = ParameterBuilder.newBuilder().apply { setGrantType(ParameterBuilder.GRANT_TYPE_PASSKEY) set(AUTH_SESSION_KEY, authSession) - addAll(parameters) + setRealm(realm) }.asDictionary() return loginWithToken(params) @@ -185,19 +199,32 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe /** - * Register a user and returns a challenge. + * Sign-up a user and returns a challenge for private and public key generation. + * 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. * - * @param userMetadata user information of the client - * @param parameters additional parameter to be sent as part of the request - * @return a request to configure and start that will yield [PasskeyRegistrationResponse] + * Example usage: + * + * + * ``` + * client.signupWithPasskey("{userData}","{realm}") + * .addParameter("scope","scope") + * .start(object: Callback { + * override fun onSuccess(result: PasskeyRegistration) { } + * override fun onFailure(error: AuthenticationException) { } + * }) + * ``` + * + * @param userData user information of the client + * @param realm default connection to use + * @return a request to configure and start that will yield [PasskeyRegistrationChallenge] */ - internal fun signupWithPasskey( - userMetadata: UserMetadataRequest, - parameters: Map, - ): Request { - val user = Gson().toJsonTree(userMetadata) + public fun signupWithPasskey( + userData: UserData, + realm: String + ): Request { + val user = Gson().toJsonTree(userData) val url = auth0.getDomainUrl().toHttpUrl().newBuilder() .addPathSegment(PASSKEY_PATH) .addPathSegment(REGISTER_PATH) @@ -205,32 +232,41 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe val params = ParameterBuilder.newBuilder().apply { setClientId(clientId) - parameters[ParameterBuilder.REALM_KEY]?.let { - setRealm(it) - } + setRealm(realm) }.asDictionary() - val passkeyRegistrationAdapter: JsonAdapter = GsonAdapter( - PasskeyRegistrationResponse::class.java, gson - ) - val post = factory.post(url.toString(), passkeyRegistrationAdapter) - .addParameters(params) as BaseRequest + val passkeyRegistrationChallengeAdapter: JsonAdapter = + GsonAdapter( + PasskeyRegistrationChallenge::class.java, gson + ) + val post = factory.post(url.toString(), passkeyRegistrationChallengeAdapter) + .addParameters(params) as BaseRequest post.addParameter(USER_PROFILE_KEY, user) return post } /** - * Request for a challenge to initiate a passkey login flow + * Request for a challenge to initiate passkey login flow * 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. * - * @param realm An optional connection name - * @return a request to configure and start that will yield [PasskeyChallengeResponse] + * Example usage: + * + * ``` + * client.passkeyChallenge("{realm}") + * .start(object: Callback { + * override fun onSuccess(result: PasskeyChallenge) { } + * override fun onFailure(error: AuthenticationException) { } + * }) + * ``` + * + * @param realm A default connection name + * @return a request to configure and start that will yield [PasskeyChallenge] */ - internal fun passkeyChallenge( - realm: String? - ): Request { + public fun passkeyChallenge( + realm: String + ): Request { val url = auth0.getDomainUrl().toHttpUrl().newBuilder() .addPathSegment(PASSKEY_PATH) .addPathSegment(CHALLENGE_PATH) @@ -238,11 +274,11 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe val parameters = ParameterBuilder.newBuilder().apply { setClientId(clientId) - realm?.let { setRealm(it) } + setRealm(realm) }.asDictionary() - val passkeyChallengeAdapter: JsonAdapter = GsonAdapter( - PasskeyChallengeResponse::class.java, gson + val passkeyChallengeAdapter: JsonAdapter = GsonAdapter( + PasskeyChallenge::class.java, gson ) return factory.post(url.toString(), passkeyChallengeAdapter) diff --git a/auth0/src/main/java/com/auth0/android/provider/PasskeyAuthProvider.kt b/auth0/src/main/java/com/auth0/android/provider/PasskeyAuthProvider.kt index fff14528..81caef8a 100644 --- a/auth0/src/main/java/com/auth0/android/provider/PasskeyAuthProvider.kt +++ b/auth0/src/main/java/com/auth0/android/provider/PasskeyAuthProvider.kt @@ -9,7 +9,7 @@ import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException import com.auth0.android.authentication.ParameterBuilder import com.auth0.android.callback.Callback -import com.auth0.android.request.UserMetadataRequest +import com.auth0.android.request.UserData import com.auth0.android.result.Credentials import java.util.concurrent.Executor import java.util.concurrent.Executors @@ -99,12 +99,12 @@ public object PasskeyAuthProvider { callback.onFailure(ex) return } - val passkeyManager = - PasskeyManager( - AuthenticationAPIClient(auth0), - CredentialManager.create(context) - ) - passkeyManager.signin(context, parameters, callback, executor) + val passkeyManager = PasskeyManager( + AuthenticationAPIClient(auth0), CredentialManager.create(context) + ) + passkeyManager.signin( + context, parameters[ParameterBuilder.REALM_KEY], parameters, callback, executor + ) } } @@ -211,14 +211,17 @@ public object PasskeyAuthProvider { callback.onFailure(ex) return } - val passkeyManager = - PasskeyManager( - AuthenticationAPIClient(auth0), - CredentialManager.create(context) - ) - val userMetadata = UserMetadataRequest(email, phoneNumber, username, name) + val passkeyManager = PasskeyManager( + AuthenticationAPIClient(auth0), CredentialManager.create(context) + ) + val userData = UserData(email, phoneNumber, username, name) passkeyManager.signup( - context, userMetadata, parameters, callback, executor + context, + userData, + parameters[ParameterBuilder.REALM_KEY], + parameters, + callback, + executor ) } } 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 1b68fd6b..6c5fd7b8 100644 --- a/auth0/src/main/java/com/auth0/android/provider/PasskeyManager.kt +++ b/auth0/src/main/java/com/auth0/android/provider/PasskeyManager.kt @@ -26,13 +26,12 @@ import androidx.credentials.exceptions.GetCredentialUnsupportedException import androidx.credentials.exceptions.NoCredentialException import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException -import com.auth0.android.authentication.ParameterBuilder import com.auth0.android.callback.Callback -import com.auth0.android.request.PublicKeyCredentialResponse -import com.auth0.android.request.UserMetadataRequest +import com.auth0.android.request.PublicKeyCredentials +import com.auth0.android.request.UserData import com.auth0.android.result.Credentials -import com.auth0.android.result.PasskeyChallengeResponse -import com.auth0.android.result.PasskeyRegistrationResponse +import com.auth0.android.result.PasskeyChallenge +import com.auth0.android.result.PasskeyRegistrationChallenge import com.google.gson.Gson import java.util.concurrent.Executor import java.util.concurrent.Executors @@ -49,15 +48,21 @@ internal class PasskeyManager( @SuppressLint("PublicKeyCredential") fun signup( context: Context, - userMetadata: UserMetadataRequest, + userData: UserData, + realm: String?, parameters: Map, callback: Callback, executor: Executor = Executors.newSingleThreadExecutor() ) { - authenticationAPIClient.signupWithPasskey(userMetadata, parameters) - .start(object : Callback { - override fun onSuccess(result: PasskeyRegistrationResponse) { + if (realm == null) { + callback.onFailure(AuthenticationException("Realm is required for passkey authentication")) + return + } + authenticationAPIClient.signupWithPasskey(userData, realm) + .addParameters(parameters) + .start(object : Callback { + override fun onSuccess(result: PasskeyRegistrationChallenge) { val pasKeyRegistrationResponse = result val request = CreatePublicKeyCredentialRequest( Gson().toJson( @@ -83,12 +88,16 @@ internal class PasskeyManager( response = result as CreatePublicKeyCredentialResponse val authRequest = Gson().fromJson( response?.registrationResponseJson, - PublicKeyCredentialResponse::class.java + PublicKeyCredentials::class.java ) + authenticationAPIClient.signinWithPasskey( - pasKeyRegistrationResponse.authSession, authRequest, parameters + pasKeyRegistrationResponse.authSession, + authRequest, + realm ) .validateClaims() + .addParameters(parameters) .start(callback) } }) @@ -106,13 +115,18 @@ internal class PasskeyManager( @RequiresApi(api = Build.VERSION_CODES.P) fun signin( context: Context, + realm: String?, parameters: Map, callback: Callback, executor: Executor = Executors.newSingleThreadExecutor() ) { - authenticationAPIClient.passkeyChallenge(parameters[ParameterBuilder.REALM_KEY]) - .start(object : Callback { - override fun onSuccess(result: PasskeyChallengeResponse) { + if (realm == null) { + callback.onFailure(AuthenticationException("Realm is required for passkey authentication")) + return + } + authenticationAPIClient.passkeyChallenge(realm) + .start(object : Callback { + override fun onSuccess(result: PasskeyChallenge) { val passkeyChallengeResponse = result val request = GetPublicKeyCredentialOption(Gson().toJson(passkeyChallengeResponse.authParamsPublicKey)) @@ -135,14 +149,15 @@ internal class PasskeyManager( is PublicKeyCredential -> { val authRequest = Gson().fromJson( credential.authenticationResponseJson, - PublicKeyCredentialResponse::class.java + PublicKeyCredentials::class.java ) authenticationAPIClient.signinWithPasskey( passkeyChallengeResponse.authSession, authRequest, - parameters + realm ) .validateClaims() + .addParameters(parameters) .start(callback) } diff --git a/auth0/src/main/java/com/auth0/android/request/PublicKeyCredentialRequest.kt b/auth0/src/main/java/com/auth0/android/request/PublicKeyCredentials.kt similarity index 95% rename from auth0/src/main/java/com/auth0/android/request/PublicKeyCredentialRequest.kt rename to auth0/src/main/java/com/auth0/android/request/PublicKeyCredentials.kt index ddedf864..063da143 100644 --- a/auth0/src/main/java/com/auth0/android/request/PublicKeyCredentialRequest.kt +++ b/auth0/src/main/java/com/auth0/android/request/PublicKeyCredentials.kt @@ -3,7 +3,7 @@ package com.auth0.android.request import com.google.gson.annotations.SerializedName -internal data class PublicKeyCredentialResponse( +public data class PublicKeyCredentials( @SerializedName("authenticatorAttachment") val authenticatorAttachment: String, @SerializedName("clientExtensionResults") diff --git a/auth0/src/main/java/com/auth0/android/request/UserData.kt b/auth0/src/main/java/com/auth0/android/request/UserData.kt new file mode 100644 index 00000000..693b6436 --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/request/UserData.kt @@ -0,0 +1,17 @@ +package com.auth0.android.request + +import com.google.gson.annotations.SerializedName + +/** + * User information for registering user when signing up using passkey. + * @param email the email of the user. email can be optional, required, or forbidden depending on the attribute configuration for the database + * @param phoneNumber the phone number of the user. phone number can be optional, required, or forbidden depending on the attribute configuration for the database + * @param userName the username of the user. username can be optional, required, or forbidden depending on the attribute configuration for the database + * @param name optional display name + */ +public data class UserData( + @field:SerializedName("email") val email: String? = null, + @field:SerializedName("phone_number") val phoneNumber: String? = null, + @field:SerializedName("username") val userName: String? = null, + @field:SerializedName("name") val name: String? = null, +) \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/request/UserMetadataRequest.kt b/auth0/src/main/java/com/auth0/android/request/UserMetadataRequest.kt deleted file mode 100644 index 2a1f808c..00000000 --- a/auth0/src/main/java/com/auth0/android/request/UserMetadataRequest.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.auth0.android.request - -import com.google.gson.annotations.SerializedName - -/** - * User metadata request used in Passkey authentication - */ -internal data class UserMetadataRequest( - @field:SerializedName("email") val email: String? = null, - @field:SerializedName("phone_number") val phoneNumber: String? = null, - @field:SerializedName("username") val userName: String? = null, - @field:SerializedName("name") val name: String? = null, -) \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/result/PasskeyChallengeResponse.kt b/auth0/src/main/java/com/auth0/android/result/PasskeyChallenge.kt similarity index 76% rename from auth0/src/main/java/com/auth0/android/result/PasskeyChallengeResponse.kt rename to auth0/src/main/java/com/auth0/android/result/PasskeyChallenge.kt index 8c49afde..84a4cabc 100644 --- a/auth0/src/main/java/com/auth0/android/result/PasskeyChallengeResponse.kt +++ b/auth0/src/main/java/com/auth0/android/result/PasskeyChallenge.kt @@ -3,14 +3,17 @@ package com.auth0.android.result import com.google.gson.annotations.SerializedName -internal data class PasskeyChallengeResponse( +/** + * Represents a challenge when user tries to login via passkeys. + */ +public data class PasskeyChallenge( @SerializedName("auth_session") val authSession: String, @SerializedName("authn_params_public_key") val authParamsPublicKey: AuthParamsPublicKey ) -internal data class AuthParamsPublicKey( +public data class AuthParamsPublicKey( @SerializedName("challenge") val challenge: String, @SerializedName("rpId") diff --git a/auth0/src/main/java/com/auth0/android/result/PasskeyRegistrationResponse.kt b/auth0/src/main/java/com/auth0/android/result/PasskeyRegistrationChallenge.kt similarity index 79% rename from auth0/src/main/java/com/auth0/android/result/PasskeyRegistrationResponse.kt rename to auth0/src/main/java/com/auth0/android/result/PasskeyRegistrationChallenge.kt index 42f778e7..e0b7d3c5 100644 --- a/auth0/src/main/java/com/auth0/android/result/PasskeyRegistrationResponse.kt +++ b/auth0/src/main/java/com/auth0/android/result/PasskeyRegistrationChallenge.kt @@ -3,14 +3,17 @@ package com.auth0.android.result import com.google.gson.annotations.SerializedName -internal data class PasskeyRegistrationResponse( +/** + * Represents a challenge when user tries to register via passkeys. + */ +public data class PasskeyRegistrationChallenge( @SerializedName("auth_session") val authSession: String, @SerializedName("authn_params_public_key") val authParamsPublicKey: AuthnParamsPublicKey ) -internal data class AuthnParamsPublicKey( +public data class AuthnParamsPublicKey( @SerializedName("authenticatorSelection") val authenticatorSelection: AuthenticatorSelection, @SerializedName("challenge") @@ -25,28 +28,28 @@ internal data class AuthnParamsPublicKey( val user: PasskeyUser ) -internal data class AuthenticatorSelection( +public data class AuthenticatorSelection( @SerializedName("residentKey") val residentKey: String, @SerializedName("userVerification") val userVerification: String ) -internal data class PubKeyCredParam( +public data class PubKeyCredParam( @SerializedName("alg") val alg: Int, @SerializedName("type") val type: String ) -internal data class RelyingParty( +public data class RelyingParty( @SerializedName("id") val id: String, @SerializedName("name") val name: String ) -internal data class PasskeyUser( +public data class PasskeyUser( @SerializedName("displayName") val displayName: String, @SerializedName("id") 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 5830ba43..0bb0c77d 100755 --- a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt @@ -9,14 +9,12 @@ import com.auth0.android.request.HttpMethod import com.auth0.android.request.NetworkingClient import com.auth0.android.request.RequestOptions import com.auth0.android.request.ServerResponse -import com.auth0.android.request.UserMetadataRequest import com.auth0.android.request.internal.RequestFactory import com.auth0.android.request.internal.ThreadSwitcherShadow import com.auth0.android.result.Authentication import com.auth0.android.result.Challenge import com.auth0.android.result.Credentials import com.auth0.android.result.DatabaseUser -import com.auth0.android.result.PasskeyRegistrationResponse import com.auth0.android.result.UserProfile import com.auth0.android.util.Auth0UserAgent import com.auth0.android.util.AuthenticationAPIMockServer @@ -26,7 +24,6 @@ import com.auth0.android.util.MockAuthenticationCallback import com.auth0.android.util.SSLTestUtils.testClient import com.google.gson.Gson import com.google.gson.GsonBuilder -import com.google.gson.JsonElement import com.google.gson.reflect.TypeToken import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.argumentCaptor @@ -194,7 +191,7 @@ public class AuthenticationAPIClientTest { val callback = MockAuthenticationCallback() val auth0 = auth0 val client = AuthenticationAPIClient(auth0) - client.signinWithPasskey("auth-session", mock(), emptyMap()) + client.signinWithPasskey("auth-session", mock(), MY_CONNECTION) .start(callback) ShadowLooper.idleMainLooper() assertThat( @@ -226,7 +223,7 @@ public class AuthenticationAPIClientTest { val client = AuthenticationAPIClient(auth0) val registrationResponse = client.signupWithPasskey( mock(), - mapOf("realm" to MY_CONNECTION) + MY_CONNECTION ) .execute() val request = mockAPI.takeRequest() 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 a5f53f74..72cf4814 100644 --- a/auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt @@ -17,13 +17,13 @@ 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.UserMetadataRequest +import com.auth0.android.request.UserData import com.auth0.android.result.AuthParamsPublicKey import com.auth0.android.result.AuthenticatorSelection import com.auth0.android.result.AuthnParamsPublicKey import com.auth0.android.result.Credentials -import com.auth0.android.result.PasskeyChallengeResponse -import com.auth0.android.result.PasskeyRegistrationResponse +import com.auth0.android.result.PasskeyChallenge +import com.auth0.android.result.PasskeyRegistrationChallenge import com.auth0.android.result.PasskeyUser import com.auth0.android.result.PubKeyCredParam import com.auth0.android.result.RelyingParty @@ -71,7 +71,7 @@ public class PasskeyManagerTest { private val exceptionCaptor: KArgumentCaptor = argumentCaptor() - private val passkeyRegistrationResponse = PasskeyRegistrationResponse( + private val passkeyRegistrationChallengeResponse = PasskeyRegistrationChallenge( authSession = "dummyAuthSession", authParamsPublicKey = AuthnParamsPublicKey( authenticatorSelection = AuthenticatorSelection( @@ -110,7 +110,7 @@ public class PasskeyManagerTest { } """ - private val passkeyChallengeResponse = PasskeyChallengeResponse( + private val passkeyChallenge = PasskeyChallenge( authSession = "authSession", authParamsPublicKey = AuthParamsPublicKey( challenge = "challenge", @@ -129,11 +129,11 @@ public class PasskeyManagerTest { @Test public fun shouldSignUpWithPasskeySuccess() { - val userMetadata: UserMetadataRequest = mock() - val parameters = mapOf("realm" to "testRealm") + val userMetadata: UserData = mock() + val parameters = mapOf("scope" to "profile") - `when`(authenticationAPIClient.signupWithPasskey(userMetadata, parameters)).thenReturn( - RequestMock(passkeyRegistrationResponse, null) + `when`(authenticationAPIClient.signupWithPasskey(userMetadata, "testRealm")).thenReturn( + RequestMock(passkeyRegistrationChallengeResponse, null) ) `when`(authenticationAPIClient.signinWithPasskey(any(), any(), any())).thenReturn( AuthenticationRequestMock( @@ -167,9 +167,16 @@ public class PasskeyManagerTest { ) } - passkeyManager.signup(context, userMetadata, parameters, callback, serialExecutor) + passkeyManager.signup( + context, + userMetadata, + "testRealm", + parameters, + callback, + serialExecutor + ) - verify(authenticationAPIClient).signupWithPasskey(userMetadata, parameters) + verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm") verify(credentialManager).createCredentialAsync(eq(context), any(), any(), any(), any()) verify(authenticationAPIClient).signinWithPasskey(any(), any(), any()) verify(callback).onSuccess(credentialsCaptor.capture()) @@ -180,17 +187,24 @@ public class PasskeyManagerTest { @Test public fun shouldSignUpWithPasskeyApiFailure() { - val userMetadata: UserMetadataRequest = mock() - val parameters = mapOf("realm" to "testRealm") + val userMetadata: UserData = mock() + val parameters = mapOf("scope" to "profile") val error = AuthenticationException("Signup failed") `when`( authenticationAPIClient.signupWithPasskey( userMetadata, - parameters + "testRealm" ) ).thenReturn(RequestMock(null, error)) - passkeyManager.signup(context, userMetadata, parameters, callback, serialExecutor) - verify(authenticationAPIClient).signupWithPasskey(userMetadata, parameters) + passkeyManager.signup( + context, + userMetadata, + "testRealm", + parameters, + callback, + serialExecutor + ) + verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm") verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any()) verify(credentialManager, never()).createCredentialAsync( any(), @@ -204,14 +218,14 @@ public class PasskeyManagerTest { @Test public fun shouldSignUpWithPasskeyCreateCredentialFailure() { - val userMetadata: UserMetadataRequest = mock() - val parameters = mapOf("realm" to "testRealm") + val userMetadata: UserData = mock() + val parameters = mapOf("scope" to "scope") `when`( authenticationAPIClient.signupWithPasskey( userMetadata, - parameters + "testRealm" ) - ).thenReturn(RequestMock(passkeyRegistrationResponse, null)) + ).thenReturn(RequestMock(passkeyRegistrationChallengeResponse, null)) whenever( credentialManager.createCredentialAsync( @@ -227,8 +241,15 @@ public class PasskeyManagerTest { ) } - passkeyManager.signup(context, userMetadata, parameters, callback, serialExecutor) - verify(authenticationAPIClient).signupWithPasskey(userMetadata, parameters) + passkeyManager.signup( + context, + userMetadata, + "testRealm", + parameters, + callback, + serialExecutor + ) + verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm") verify(credentialManager).createCredentialAsync(eq(context), any(), any(), any(), any()) verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any()) verify(callback).onFailure(exceptionCaptor.capture()) @@ -245,11 +266,11 @@ public class PasskeyManagerTest { @Test public fun shouldSignInWithPasskeySuccess() { - val parameters = mapOf("realm" to "testRealm") + val parameters = mapOf("scope" to "scope") val credentialResponse: GetCredentialResponse = mock() - `when`(authenticationAPIClient.passkeyChallenge(parameters["realm"])).thenReturn( - RequestMock(passkeyChallengeResponse, null) + `when`(authenticationAPIClient.passkeyChallenge("testRealm")).thenReturn( + RequestMock(passkeyChallenge, null) ) `when`(credentialResponse.credential).thenReturn( @@ -278,9 +299,9 @@ public class PasskeyManagerTest { }.`when`(credentialManager) .getCredentialAsync(any(), any(), any(), any(), any()) - passkeyManager.signin(context, parameters, callback, serialExecutor) + passkeyManager.signin(context, "testRealm", parameters, callback, serialExecutor) - verify(authenticationAPIClient).passkeyChallenge(parameters["realm"]) + verify(authenticationAPIClient).passkeyChallenge("testRealm") verify(credentialManager).getCredentialAsync( any(), any(), @@ -297,14 +318,14 @@ public class PasskeyManagerTest { @Test public fun shouldSignInWithPasskeyApiFailure() { - val parameters = mapOf("realm" to "testRealm") + val parameters = mapOf("scope" to "profile") val error = AuthenticationException("Signin failed") - `when`(authenticationAPIClient.passkeyChallenge(parameters["realm"])).thenReturn( + `when`(authenticationAPIClient.passkeyChallenge("testRealm")).thenReturn( RequestMock(null, error) ) - passkeyManager.signin(context, parameters, callback, serialExecutor) + passkeyManager.signin(context, "testRealm", parameters, callback, serialExecutor) verify(authenticationAPIClient).passkeyChallenge(any()) verify(credentialManager, never()).getCredentialAsync( @@ -321,8 +342,8 @@ public class PasskeyManagerTest { @Test public fun shouldSignInWithPasskeyGetCredentialFailure() { val parameters = mapOf("realm" to "testRealm") - `when`(authenticationAPIClient.passkeyChallenge(parameters["realm"])).thenReturn( - RequestMock(passkeyChallengeResponse, null) + `when`(authenticationAPIClient.passkeyChallenge("testRealm")).thenReturn( + RequestMock(passkeyChallenge, null) ) whenever( @@ -339,8 +360,8 @@ public class PasskeyManagerTest { ) } - passkeyManager.signin(context, parameters, callback, serialExecutor) - verify(authenticationAPIClient).passkeyChallenge(parameters["realm"]) + passkeyManager.signin(context, "testRealm", parameters, callback, serialExecutor) + verify(authenticationAPIClient).passkeyChallenge("testRealm") verify(credentialManager).getCredentialAsync( any(), any(), diff --git a/sample/build.gradle b/sample/build.gradle index 1abd84c3..bce846c7 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -52,7 +52,10 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' + implementation "androidx.credentials:credentials-play-services-auth:1.3.0" + implementation "androidx.credentials:credentials:1.3.0" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.google.code.gson:gson:2.8.9' } \ No newline at end of file diff --git a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt index 4546908c..67e01b24 100644 --- a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt +++ b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt @@ -1,9 +1,21 @@ package com.auth0.sample import android.os.Bundle +import android.os.CancellationSignal import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.credentials.CreateCredentialResponse +import androidx.credentials.CreatePublicKeyCredentialRequest +import androidx.credentials.CreatePublicKeyCredentialResponse +import androidx.credentials.CredentialManager +import androidx.credentials.CredentialManagerCallback +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse +import androidx.credentials.GetPublicKeyCredentialOption +import androidx.credentials.PublicKeyCredential +import androidx.credentials.exceptions.CreateCredentialException +import androidx.credentials.exceptions.GetCredentialException import androidx.fragment.app.Fragment import com.auth0.android.Auth0 import com.auth0.android.authentication.AuthenticationAPIClient @@ -17,16 +29,21 @@ import com.auth0.android.authentication.storage.SharedPreferencesStorage import com.auth0.android.callback.Callback import com.auth0.android.management.ManagementException import com.auth0.android.management.UsersAPIClient -import com.auth0.android.provider.PasskeyAuthProvider import com.auth0.android.provider.WebAuthProvider import com.auth0.android.request.DefaultClient +import com.auth0.android.request.PublicKeyCredentials +import com.auth0.android.request.UserData import com.auth0.android.result.Credentials +import com.auth0.android.result.PasskeyChallenge +import com.auth0.android.result.PasskeyRegistrationChallenge import com.auth0.android.result.UserProfile import com.auth0.sample.databinding.FragmentDatabaseLoginBinding import com.google.android.material.snackbar.Snackbar +import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import java.util.concurrent.Executors /** * A simple [Fragment] subclass as the default destination in the navigation. @@ -50,6 +67,10 @@ class DatabaseLoginFragment : Fragment() { "https://${getString(R.string.com_auth0_domain)}/api/v2/" } + private val credentialManager: CredentialManager by lazy { + CredentialManager.create(requireContext()) + } + private val authenticationApiClient: AuthenticationAPIClient by lazy { AuthenticationAPIClient(account) } @@ -57,7 +78,10 @@ class DatabaseLoginFragment : Fragment() { private val secureCredentialsManager: SecureCredentialsManager by lazy { val storage = SharedPreferencesStorage(requireContext()) val manager = SecureCredentialsManager( - requireContext(), account, storage, requireActivity(), + requireContext(), + account, + storage, + requireActivity(), localAuthenticationOptions ) manager @@ -70,14 +94,16 @@ class DatabaseLoginFragment : Fragment() { } private val localAuthenticationOptions = - LocalAuthenticationOptions.Builder().setTitle("Biometric").setDescription("description") - .setAuthenticationLevel(AuthenticationLevel.STRONG).setNegativeButtonText("Cancel") + LocalAuthenticationOptions.Builder() + .setTitle("Biometric") + .setDescription("description") + .setAuthenticationLevel(AuthenticationLevel.STRONG) + .setNegativeButtonText("Cancel") .setDeviceCredentialFallback(true) .build() override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { val binding = FragmentDatabaseLoginBinding.inflate(inflater, container, false) binding.btLogin.setOnClickListener { @@ -94,56 +120,23 @@ class DatabaseLoginFragment : Fragment() { } binding.btSignupPasskey.setOnClickListener { - PasskeyAuthProvider.signUp(account) - .setEmail("username@email.com") - .setRealm("Username-Password-Authentication") - .start( - requireActivity(), - object : Callback { - override fun onSuccess(result: Credentials) { - credentialsManager.saveCredentials(result) - Snackbar.make( - requireView(), - "Hello ${result.user.name}", - Snackbar.LENGTH_LONG - ).show() - } - - override fun onFailure(error: AuthenticationException) { - Snackbar.make( - requireView(), - error.getDescription(), - Snackbar.LENGTH_LONG - ) - .show() - } - }) + passkeySignup() } - binding.btSignInPasskey.setOnClickListener { - PasskeyAuthProvider - .signIn(account) - .setRealm("Username-Password-Authentication") - .start(requireActivity(), object : Callback { - override fun onSuccess(result: Credentials) { - credentialsManager.saveCredentials(result) - Snackbar.make( - requireView(), - "Hello ${result.user.name}", - Snackbar.LENGTH_LONG - ).show() - } - override fun onFailure(error: AuthenticationException) { - Snackbar.make( - requireView(), - error.getDescription(), - Snackbar.LENGTH_LONG - ) - .show() - } + binding.btSignInPasskey.setOnClickListener { + passkeySignin() + } - }) + binding.btSignupPasskeyAsync.setOnClickListener { + launchAsync { + passkeySignupAsync() + } + } + binding.btSigninPasskeyAsync.setOnClickListener { + launchAsync { + passkeySigninAsync() + } } binding.btWebAuth.setOnClickListener { @@ -208,10 +201,10 @@ class DatabaseLoginFragment : Fragment() { requireView(), "Hello ${result.user.name}", Snackbar.LENGTH_LONG - ).show() - } catch (error: AuthenticationException) { - Snackbar.make(requireView(), error.getDescription(), Snackbar.LENGTH_LONG) + ) .show() + } catch (error: AuthenticationException) { + Snackbar.make(requireView(), error.getDescription(), Snackbar.LENGTH_LONG).show() } } @@ -256,28 +249,33 @@ class DatabaseLoginFragment : Fragment() { override fun onFailure(error: AuthenticationException) { val message = - if (error.isCanceled) "Browser was closed" else error.getDescription() - Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG).show() + if (error.isCanceled) + "Browser was closed" + else + error.getDescription() + Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG) + .show() } }) } private suspend fun webAuthAsync() { try { - val credentials = WebAuthProvider.login(account) - .withScheme(getString(R.string.com_auth0_scheme)) - .withAudience(audience) - .withScope(scope) - .await(requireContext()) + val credentials = + WebAuthProvider.login(account) + .withScheme(getString(R.string.com_auth0_scheme)) + .withAudience(audience) + .withScope(scope) + .await(requireContext()) credentialsManager.saveCredentials(credentials) Snackbar.make( - requireView(), - "Hello ${credentials.user.name}", - Snackbar.LENGTH_LONG + requireView(), "Hello ${credentials.user.name}", Snackbar.LENGTH_LONG ).show() } catch (error: AuthenticationException) { - val message = - if (error.isCanceled) "Browser was closed" else error.getDescription() + val message = if (error.isCanceled) + "Browser was closed" + else + error.getDescription() Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG).show() } } @@ -288,9 +286,7 @@ class DatabaseLoginFragment : Fragment() { .start(requireContext(), object : Callback { override fun onSuccess(result: Void?) { Snackbar.make( - requireView(), - "Logged out", - Snackbar.LENGTH_LONG + requireView(), "Logged out", Snackbar.LENGTH_LONG ).show() } @@ -309,13 +305,10 @@ class DatabaseLoginFragment : Fragment() { .withScheme(getString(R.string.com_auth0_scheme)) .await(requireContext()) Snackbar.make( - requireView(), - "Logged out", - Snackbar.LENGTH_LONG + requireView(), "Logged out", Snackbar.LENGTH_LONG ).show() } catch (error: AuthenticationException) { - val message = - if (error.isCanceled) "Browser was closed" else error.getDescription() + val message = if (error.isCanceled) "Browser was closed" else error.getDescription() Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG).show() } } @@ -325,8 +318,7 @@ class DatabaseLoginFragment : Fragment() { } private fun getCreds() { - credentialsManager.getCredentials( - null, + credentialsManager.getCredentials(null, 300, emptyMap(), emptyMap(), @@ -347,49 +339,42 @@ class DatabaseLoginFragment : Fragment() { } private fun getCredsSecure() { - secureCredentialsManager.getCredentials( - object : - Callback { - override fun onSuccess(result: Credentials) { - Snackbar.make( - requireView(), - "Got credentials - ${result.accessToken}", - Snackbar.LENGTH_LONG - ).show() - } + secureCredentialsManager.getCredentials(object : + Callback { + override fun onSuccess(result: Credentials) { + Snackbar.make( + requireView(), "Got credentials - ${result.accessToken}", Snackbar.LENGTH_LONG + ).show() + } - override fun onFailure(error: CredentialsManagerException) { - Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() - when (error) { - CredentialsManagerException.NO_CREDENTIALS -> { - // handle no credentials scenario - println("NO_CREDENTIALS: $error") - } + override fun onFailure(error: CredentialsManagerException) { + Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() + when (error) { + CredentialsManagerException.NO_CREDENTIALS -> { + // handle no credentials scenario + println("NO_CREDENTIALS: $error") + } - CredentialsManagerException.NO_REFRESH_TOKEN -> { - // handle no refresh token scenario - println("NO_REFRESH_TOKEN: $error") - } + CredentialsManagerException.NO_REFRESH_TOKEN -> { + // handle no refresh token scenario + println("NO_REFRESH_TOKEN: $error") + } - CredentialsManagerException.STORE_FAILED -> { - // handle store failed scenario - println("STORE_FAILED: $error") - } - // ... similarly for other error codes + CredentialsManagerException.STORE_FAILED -> { + // handle store failed scenario + println("STORE_FAILED: $error") } + // ... similarly for other error codes } } - ) + }) } private suspend fun getCredsAsync() { try { - val credentials = - credentialsManager.awaitCredentials() + val credentials = credentialsManager.awaitCredentials() Snackbar.make( - requireView(), - "Got credentials - ${credentials.accessToken}", - Snackbar.LENGTH_LONG + requireView(), "Got credentials - ${credentials.accessToken}", Snackbar.LENGTH_LONG ).show() } catch (error: CredentialsManagerException) { Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() @@ -405,9 +390,7 @@ class DatabaseLoginFragment : Fragment() { .start(object : Callback { override fun onFailure(error: ManagementException) { Snackbar.make( - requireView(), - error.getDescription(), - Snackbar.LENGTH_LONG + requireView(), error.getDescription(), Snackbar.LENGTH_LONG ).show() } @@ -433,9 +416,7 @@ class DatabaseLoginFragment : Fragment() { val users = UsersAPIClient(account, credentials.accessToken) val user = users.getProfile(credentials.user.getId()!!).await() Snackbar.make( - requireView(), - "Got profile for ${user.name}", - Snackbar.LENGTH_LONG + requireView(), "Got profile for ${user.name}", Snackbar.LENGTH_LONG ).show() } catch (error: CredentialsManagerException) { Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() @@ -457,9 +438,7 @@ class DatabaseLoginFragment : Fragment() { .start(object : Callback { override fun onFailure(error: ManagementException) { Snackbar.make( - requireView(), - error.getDescription(), - Snackbar.LENGTH_LONG + requireView(), error.getDescription(), Snackbar.LENGTH_LONG ).show() } @@ -506,4 +485,246 @@ class DatabaseLoginFragment : Fragment() { runnable.invoke() } } + + private fun passkeySignup() { + authenticationApiClient.signupWithPasskey( + UserData( + email = "jndoe@email.com" + ), "Username-Password-Authentication" + ).start(object : Callback { + override fun onSuccess(result: PasskeyRegistrationChallenge) { + val passKeyRegistrationChallenge = result + val request = CreatePublicKeyCredentialRequest( + Gson().toJson( + passKeyRegistrationChallenge.authParamsPublicKey + ) + ) + var response: CreatePublicKeyCredentialResponse? + + credentialManager.createCredentialAsync(requireContext(), + request, + CancellationSignal(), + Executors.newSingleThreadExecutor(), + object : + CredentialManagerCallback { + + override fun onError(e: CreateCredentialException) { + } + + override fun onResult(result: CreateCredentialResponse) { + + response = result as CreatePublicKeyCredentialResponse + val authRequest = Gson().fromJson( + response?.registrationResponseJson, + PublicKeyCredentials::class.java + ) + + authenticationApiClient.signinWithPasskey( + passKeyRegistrationChallenge.authSession, + authRequest, + "Username-Password-Authentication" + ) + .validateClaims() + .start(object : Callback { + override fun onSuccess(result: Credentials) { + credentialsManager.saveCredentials(result) + Snackbar.make( + requireView(), + "Hello ${result.user.name}", + Snackbar.LENGTH_LONG + ).show() + } + + override fun onFailure(error: AuthenticationException) { + Snackbar.make( + requireView(), + error.getDescription(), + Snackbar.LENGTH_LONG + ).show() + } + }) + } + }) + } + + override fun onFailure(error: AuthenticationException) { + Snackbar.make( + requireView(), + error.getDescription(), + Snackbar.LENGTH_LONG + ).show() + } + }) + } + + private fun passkeySignin() { + authenticationApiClient.passkeyChallenge("Username-Password-Authentication") + .start(object : Callback { + override fun onSuccess(result: PasskeyChallenge) { + val passkeyChallengeResponse = result + val request = + GetPublicKeyCredentialOption(Gson().toJson(passkeyChallengeResponse.authParamsPublicKey)) + val getCredRequest = GetCredentialRequest( + listOf(request) + ) + + credentialManager.getCredentialAsync(requireContext(), + getCredRequest, + CancellationSignal(), + Executors.newSingleThreadExecutor(), + object : + CredentialManagerCallback { + override fun onError(e: GetCredentialException) { + } + + override fun onResult(result: GetCredentialResponse) { + when (val credential = result.credential) { + is PublicKeyCredential -> { + val authRequest = Gson().fromJson( + credential.authenticationResponseJson, + PublicKeyCredentials::class.java + ) + authenticationApiClient.signinWithPasskey( + passkeyChallengeResponse.authSession, + authRequest, + "Username-Password-Authentication" + ) + .validateClaims() + .start(object : + Callback { + override fun onSuccess(result: Credentials) { + credentialsManager.saveCredentials(result) + Snackbar.make( + requireView(), + "Hello ${result.user.name}", + Snackbar.LENGTH_LONG + ).show() + } + + override fun onFailure(error: AuthenticationException) { + Snackbar.make( + requireView(), + error.getDescription(), + Snackbar.LENGTH_LONG + ).show() + } + }) + } + + else -> { + Snackbar.make( + requireView(), + "Received unrecognized credential type ${credential.type}.This shouldn't happen", + Snackbar.LENGTH_LONG + ).show() + } + } + } + }) + } + + override fun onFailure(error: AuthenticationException) { + Snackbar.make( + requireView(), error.getDescription(), Snackbar.LENGTH_LONG + ).show() + } + }) + } + + private suspend fun passkeySignupAsync() { + + try { + val challenge = authenticationApiClient.signupWithPasskey( + UserData(email = "jdoe@email.com"), + "Username-Password-Authentication" + ).await() + + val request = CreatePublicKeyCredentialRequest( + Gson().toJson(challenge.authParamsPublicKey) + ) + + val result = credentialManager.createCredential(requireContext(), request) + + val authRequest = Gson().fromJson( + (result as CreatePublicKeyCredentialResponse).registrationResponseJson, + PublicKeyCredentials::class.java + ) + + val userCredential = authenticationApiClient.signinWithPasskey( + challenge.authSession, authRequest, "Username-Password-Authentication" + ) + .validateClaims() + .await() + + credentialsManager.saveCredentials(userCredential) + Snackbar.make( + requireView(), + "Hello ${userCredential.user.name}", + Snackbar.LENGTH_LONG + ).show() + + } catch (e: CreateCredentialException) { + Snackbar.make( + requireView(), + e.errorMessage!!, + Snackbar.LENGTH_LONG + ).show() + } catch (exception: AuthenticationException) { + Snackbar.make( + requireView(), + exception.getDescription(), + Snackbar.LENGTH_LONG + ).show() + } + } + + private suspend fun passkeySigninAsync() { + try { + + val challenge = + authenticationApiClient.passkeyChallenge("Username-Password-Authentication") + .await() + + val request = GetPublicKeyCredentialOption(Gson().toJson(challenge.authParamsPublicKey)) + val getCredRequest = GetCredentialRequest( + listOf(request) + ) + val result = credentialManager.getCredential(requireContext(), getCredRequest) + when (val credential = result.credential) { + is PublicKeyCredential -> { + val authRequest = Gson().fromJson( + credential.authenticationResponseJson, + PublicKeyCredentials::class.java + ) + val userCredential = authenticationApiClient.signinWithPasskey( + challenge.authSession, + authRequest, + "Username-Password-Authentication" + ) + .validateClaims() + .await() + credentialsManager.saveCredentials(userCredential) + Snackbar.make( + requireView(), + "Hello ${userCredential.user.name}", + Snackbar.LENGTH_LONG + ).show() + } + + else -> {} + } + } catch (e: GetCredentialException) { + Snackbar.make( + requireView(), + e.errorMessage!!, + Snackbar.LENGTH_LONG + ).show() + } catch (exception: AuthenticationException) { + Snackbar.make( + requireView(), + exception.getDescription(), + Snackbar.LENGTH_LONG + ).show() + } + } } \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_database_login.xml b/sample/src/main/res/layout/fragment_database_login.xml index 61d1a7bc..5d3731b2 100644 --- a/sample/src/main/res/layout/fragment_database_login.xml +++ b/sample/src/main/res/layout/fragment_database_login.xml @@ -20,8 +20,8 @@ android:autofillHints="emailAddress" android:ems="10" android:hint="Email" - android:isCredential="true" android:inputType="textEmailAddress" + android:isCredential="true" android:text="asd@asd.asd" android:textSize="14sp" app:layout_constraintEnd_toEndOf="parent" @@ -38,8 +38,8 @@ android:autofillHints="password" android:ems="10" android:hint="Password" - android:isCredential="true" android:inputType="textPassword" + android:isCredential="true" android:text="asdasd" android:textSize="14sp" app:layout_constraintEnd_toEndOf="parent" @@ -69,7 +69,7 @@ android:textSize="16sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/btSignInPasskey" /> + app:layout_constraintTop_toBottomOf="@+id/btSigninPasskeyAsync" /> +