diff --git a/EXAMPLES.md b/EXAMPLES.md index 6597019d8..e92bfef1d 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -21,6 +21,8 @@ - [Get user information](#get-user-information) - [Custom Token Exchange](#custom-token-exchange) - [Native to Web SSO login [EA]](#native-to-web-sso-login-ea) + - [My Account API](#my-account-api) + - [Enroll a new passkey](#enroll-a-new-passkey) - [Credentials Manager](#credentials-manager) - [Secure Credentials Manager](#secure-credentials-manager) - [Usage](#usage) @@ -649,6 +651,115 @@ authentication +## My Account API + +> [!NOTE] +> The My Account API is currently available in [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). Please reach out to Auth0 support to get it enabled for your tenant. + +Use the Auth0 My Account API to manage the current user's account. + +To call the My Account API, you need an access token issued specifically for this API, including any required scopes for the operations you want to perform. + +### Enroll a new passkey + +**Scopes required:** `create:me:authentication_methods` + +Enrolling a new passkey is a three-step process. First, you request an enrollment challenge from Auth0. Then you need to pass that challenge to Google's [Credential Manager](https://developer.android.com/identity/sign-in/credential-manager) +APIs to create a new passkey credential. Finally, you use the created passkey credential and the original challenge to enroll the passkey with Auth0. + +#### Prerequisites + +- A custom domain configured for your Auth0 tenant. +- The **Passkeys** grant to be enabled for your Auth0 application. +- The Android **Device Settings** configured for your Auth0 application. +- Passkeys are supported only on devices that run Android 9 (API level 28) or higher. + +Check [our documentation](https://auth0.com/docs/native-passkeys-for-mobile-applications#before-you-begin) for more information. + +#### 1. Request an enrollment challenge + +You can specify an optional user identity identifier and/or a database connection name to help Auth0 find the user. The user identity identifier will be needed if the user logged in with a [linked account](https://auth0.com/docs/manage-users/user-accounts/user-account-linking). + +```kotlin + +val client = MyAccountAPIClient(account, accessToken) + +client.passkeyEnrollmentChallenge() + .start(object :Callback{ + override fun onSuccess(result: PasskeyEnrollmentChallenge) { + print("Challenge: ${result.challenge}") + } + override fun onFailure(error: MyAccountException) { + print("Error: ${error.message}") + } + }) +``` +
+ Using coroutines + +```kotlin + + val client = MyAccountAPIClient(account, "accessToken") + + try{ + val challenge = client.passkeyEnrollmentChallenge() + .await() + println("Challenge: $challenge") + } catch (exception:MyAccountException){ + print("Error: ${exception.message}") + } +``` +
+ +#### 2. Create a new passkey credential + +Use the enrollment challenge with the Google's [CredentialManager](https://developer.android.com/identity/sign-in/credential-manager) APIs to create a new passkey credential. + +```kotlin +// Using coroutines +val request = CreatePublicKeyCredentialRequest( + Gson().toJson(enrollmentChallenge.authParamsPublicKey) +) + +val result = credentialManager.createCredential(requireContext(), request) + +val passkeyCredentials = Gson().fromJson( + (result as CreatePublicKeyCredentialResponse).registrationResponseJson, + PublicKeyCredentials::class.java +) +``` +#### 3. Enroll the passkey + +Use the created passkey credential and the enrollment challenge to enroll the passkey with Auth0. + +```Kotlin + +client.enroll(passkeyCredential,challenge) + .start(object :Callback{ + override fun onSuccess(result: PasskeyAuthenticationMethod) { + println("Passkey enrolled successfully: ${result.id}") + } + + override fun onFailure(error: MyAccountException) { + println("Error enrolling passkey: ${error.message}") + } + }) +``` +
+ Using coroutines + +```kotlin + +try{ + val result = client.enroll(passkeyCredential,challenge) + .await() + println("Passkey enrolled successfully: ${result.id}") +}catch(error:MyAccountException){ + println("Error enrolling passkey: ${error.message}") +} +``` +
+ ## Credentials Manager ### Secure Credentials Manager diff --git a/sample/src/main/java/com/auth0/sample/MainFragment.kt b/sample/src/main/java/com/auth0/sample/MainFragment.kt new file mode 100644 index 000000000..c3f245b92 --- /dev/null +++ b/sample/src/main/java/com/auth0/sample/MainFragment.kt @@ -0,0 +1,179 @@ +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 android.widget.Button +import android.widget.TextView +import androidx.credentials.CreateCredentialResponse +import androidx.credentials.CreatePublicKeyCredentialRequest +import androidx.credentials.CreatePublicKeyCredentialResponse +import androidx.credentials.CredentialManager +import androidx.credentials.CredentialManagerCallback +import androidx.credentials.exceptions.CreateCredentialException +import androidx.fragment.app.Fragment +import com.auth0.android.Auth0 +import com.auth0.android.authentication.AuthenticationException +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.provider.WebAuthProvider +import com.auth0.android.request.DefaultClient +import com.auth0.android.request.PublicKeyCredentials +import com.auth0.android.result.Credentials +import com.auth0.android.result.PasskeyAuthenticationMethod +import com.auth0.android.result.PasskeyEnrollmentChallenge +import com.google.android.material.snackbar.Snackbar +import com.google.gson.Gson +import java.util.concurrent.Executors + + +class MainFragment : Fragment() { + + private lateinit var webLoginButton: Button + private lateinit var passkeyEnrollment: Button + private lateinit var accessToken: TextView + + private lateinit var credentialToken: String + + private val scope = + "openid profile email read:current_user create:me:authentication_methods update:current_user_metadata" + + private val account: Auth0 by lazy { + // -- REPLACE this credentials with your own Auth0 app credentials! + val account = Auth0.getInstance( + getString(R.string.com_auth0_client_id), + getString(R.string.com_auth0_domain) + ) + // Only enable network traffic logging on production environments! + account.networkingClient = DefaultClient(enableLogging = true) + account + } + + private val credentialManager: CredentialManager by lazy { + CredentialManager.create(requireContext()) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + webLoginButton = view.findViewById(R.id.webLogin) + passkeyEnrollment = view.findViewById(R.id.enrollment) + accessToken = view.findViewById(R.id.accessToken) + + webLoginButton.setOnClickListener { + webAuth() + } + + passkeyEnrollment.setOnClickListener { + passkeyEnroll() + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_main, container, false) + } + + + private fun webAuth() { + WebAuthProvider.login(account) + .withScheme(getString(R.string.com_auth0_scheme)) + .withAudience("AUDIENCE") + .withScope(scope) + .start(requireContext(), object : Callback { + override fun onSuccess(result: Credentials) { + accessToken.visibility = View.VISIBLE + passkeyEnrollment.visibility = View.VISIBLE + credentialToken = result.accessToken + accessToken.text = result.accessToken + } + + override fun onFailure(error: AuthenticationException) { + val message = + if (error.isCanceled) + "Browser was closed" + else + error.getDescription() + accessToken.text = message + } + }) + } + + private fun passkeyEnroll() { + + if (!this::credentialToken.isInitialized) { + Snackbar.make( + requireView(), + "Please login first", + Snackbar.LENGTH_LONG + ).show() + return + } + val client = MyAccountAPIClient(account, credentialToken) + client.passkeyEnrollmentChallenge() + .start(object : Callback { + override fun onSuccess(result: PasskeyEnrollmentChallenge) { + val challenge = result + val request = CreatePublicKeyCredentialRequest( + Gson().toJson( + challenge.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 credentials = Gson().fromJson( + response?.registrationResponseJson, + PublicKeyCredentials::class.java + ) + + client.enroll( + credentials, + challenge + ) + .start(object : + Callback { + override fun onSuccess(result: PasskeyAuthenticationMethod) { + Snackbar.make( + requireView(), + "Passkey Enrolled Successfully", + Snackbar.LENGTH_LONG + ).show() + } + + override fun onFailure(error: MyAccountException) { + Snackbar.make( + requireView(), + error.getDescription(), + Snackbar.LENGTH_LONG + ).show() + } + }) + } + }) + } + + override fun onFailure(error: MyAccountException) { + accessToken.text = "Error: ${error.getDescription()}" + } + }) + } +} \ No newline at end of file diff --git a/sample/src/main/res/drawable/rounded_corner.xml b/sample/src/main/res/drawable/rounded_corner.xml new file mode 100644 index 000000000..280ad5b8f --- /dev/null +++ b/sample/src/main/res/drawable/rounded_corner.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_main.xml b/sample/src/main/res/layout/fragment_main.xml new file mode 100644 index 000000000..19dda15a2 --- /dev/null +++ b/sample/src/main/res/layout/fragment_main.xml @@ -0,0 +1,48 @@ + + + +