Register Passkeys in Android Applications Using the Okta MyAccounts API
Last Updated:
Overview
Configure an Android application to register Passkeys outside of an authentication flow by modifying the application code and updating the Okta organization settings. The Okta Identity Engine (IDX) pipeline supports registering Passkey credentials during authenticator enrollment, but administrators may need to allow Passkey registration independently using the Okta MyAccounts API.
Applies To
- Okta Identity Engine (OIE)
- Android
- okta-mobile-kotlin SDK
- Passkeys / Web Authentication (WebAuthn)
- MyAccount API
Solution
What are the prerequisites for configuring Passkeys in Android?
Ensure the environment meets the following prerequisites before configuring the Android application.
- Set up the sample application and configure an Okta organization for the Android
AssetLinks.jsonfile. Review Configure Passkeys for Native Logins in Android Applications for more information.
Modify the Sample Application Files to Allow Passkey Enrollment
The following code builds on the okta-mobile-kotlin Identity Engine (IDX) Sample Application to allow Passkey enrollment once logged into the sample application.
Modify the AndroidManifest.xml, DynamicAuthViewModel.kt, DashboardViewModel.kt, DashboardFragment.kt, and fragment_dashboard.xml files by following these steps.
- Edit the AndroidManifest.xml file to allow internet access.
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> - Edit the DynamicAuthViewModel file to specify the required
acr_valueofurn:okta:loa:2fa:any:ifpossible.... extraRequestParameters["acr_values"] = "urn:okta:loa:2fa:any:ifpossible" // <-- add val interactionCodeFlow = InteractionCodeFlow() // Initiate the IDX client and start IDX flow. ...- NOTE: This step is only required if the Org has the feature "IDP MyAccount API 2FA If Possible" enabled.
- Edit the SampleCredentialHelper file to include the required scope
okta.myAccount.webauthn.manage.... clientId = BuildConfig.CLIENT_ID, defaultScope = "openid email profile offline_access okta.myAccount.webauthn.manage" ...
- Edit the fragment_dashboard.xml file to add a register passkey button.
... </ScrollView> <Button android:id="@+id/register_passkey_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="16dp" android:text="@string/register_passkey" /> <Button ...
- Edit the DashboardFragment file to set up the view and model connections.
... binding.signOutButton.setOnClickListener { viewModel.logout() } // add below binding.registerPasskeyButton.setOnClickListener { viewModel.registerPasskey(requireActivity(), requireContext()) } viewModel.passkeyLiveData.observe(viewLifecycleOwner) { value -> if (value != null && value.length > 0) { Toast.makeText(requireContext(), value, Toast.LENGTH_LONG).show() } } // end ...
- Edit the strings.xml file to add the text for the register passkey button.
<string name="register_passkey">Register New Passkey</string>
- Edit the DashboardViewModel file to contain the passkey registration logic.
... private lateinit var credential: Credential //add below private val _passkeyLiveData = MutableLiveData<String>("") val passkeyLiveData: LiveData<String> = _passkeyLiveData //end ... ... fun logout() { _logoutStateLiveData.value = LogoutState.Loading ... } //add below fun registerPasskey(activity: Activity, context: Context) { viewModelScope.launch { credential = Credential.default ?: run { // Null Credential, go back to login screen _passkeyLiveData.postValue("Access Token not Present") return@launch } val accessToken = credential.getValidAccessToken() if (accessToken == null) { _passkeyLiveData.postValue("Access Token not Present") return@launch } val okHttpClient = OkHttpClient() val request = buildRequest("/idp/myaccount/webauthn/registration", "", accessToken = accessToken.toString()) okHttpClient.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: java.io.IOException) { e.printStackTrace() _passkeyLiveData.postValue("Error starting Passkey Request: ${e.toString()}") } override fun onResponse(call: okhttp3.Call, response: Response) { if (!response.isSuccessful) { _passkeyLiveData.postValue("Error starting Passkey Request: ${response.code}, ${response.body?.string() ?: ""}") return } // Handle success val activationData = response.body?.string() if (activationData == null) { _passkeyLiveData.postValue("ActivationData not Received") return } createPasskey(activationData, activity, context, accessToken.toString()) } }) } } private fun createPasskey(activationData: String, activity: Activity, context: Context, accessToken: String) { val rootObject = JSONObject(activationData).getJSONObject("options") val authenticatorSelectionObject = rootObject.getJSONObject("authenticatorSelection") /* below line is only needed if the WebAuthn/Fido Authenticator in Okta does not enable autofill for Passkey support */ authenticatorSelectionObject.put("residentKey", "preferred") viewModelScope.launch { val createCredentialResponse = androidx.credentials.CredentialManager .create(activity) .createCredential(activity, CreatePublicKeyCredentialRequest(rootObject.toString())) when (createCredentialResponse) { is CreatePublicKeyCredentialResponse -> { Timber.i("CreatePublicKeyCredentialResponse.registrationResponseJson = ${createCredentialResponse.registrationResponseJson}") val response = JSONObject(createCredentialResponse.registrationResponseJson).getJSONObject("response") val requestObject = JSONObject() requestObject.put("clientData", response.getString("clientDataJSON")) requestObject.put("attestation", response.getString("attestationObject")) requestObject.put("transports", response.getString("transports")) val request = buildRequest("/idp/myaccount/webauthn", requestObject.toString(), accessToken) val okHttpClient = OkHttpClient() okHttpClient.newCall(request).enqueue(object : Callback { override fun onFailure(call: okhttp3.Call, e: IOException) { e.printStackTrace() _passkeyLiveData.postValue("Error registering Passkey: ${e.toString()}") } override fun onResponse(call: okhttp3.Call, response: Response) { if (!response.isSuccessful) { _passkeyLiveData.postValue("Error registering Passkey: ${response.code}, ${response.body?.string() ?: ""}") return } _passkeyLiveData.postValue("Passkey Enrolled: ${response.code}") } }) } else -> throw UnsupportedOperationException("Unsupported credential type ${createCredentialResponse::class.java.name}") } } } private fun buildRequest(uri: String, body: String, accessToken: String): Request { val request = Request.Builder() .url("https://${BuildConfig.ISSUER.toUri().host}${uri}") .header("Authorization", "Bearer $accessToken") .header("Content-Type", "application/json") .header("Accept", "application/json; okta-version=1.0.0") .post(body.toRequestBody("application/json".toMediaType())) .build() return request } //end fun acknowledgeLogoutSuccess() { _logoutStateLiveData.value = LogoutState.Idle } ...
What changes occur in the application code?
The code modifications introduce the following changes to the application.
- The
okta.myAccount.webauthn.managescope allows the application to make MyAccount Web Authentication (WebAuthn) API calls. - The
urn:okta:loa:2fa:any:ifpossibleAuthentication Context Class Reference (ACR) value steps up the session, allowing Okta to grant theokta.myAccount.webauthn.managescope. - The
DashboardViewModelmodifications add theregisterPasskey()andcreatePasskey()functions.- The
registerPasskey()function starts the registration process by calling the MyAccount API startWebAuthnEnrollment. - The
createPasskey()function usesandroidx.credentials.CredentialManagerto initiate credential enrollment with the activation data from Okta. Upon successful creation, the function extracts the client data and attestation, and finishes the registration process by calling the MyAccount API createWebAuthnEnrollment.
- The
NOTE: All Okta MyAccount API requests must include an HTTP Accept header with the value application/json; okta-version=1.0.0, or Okta rejects the request.
Configure the Okta Organization Settings
Update the OpenID Connect (OIDC) application and the authorization server in Okta by following these steps.
- Navigate to Security > API > Authorization Servers >
<Auth_Server>> Scopes in the Okta Admin Console. - Click Add Scope.
- Enter
okta.myAccount.webauthn.managein the Name field. - Enter an optional display name and description, and keep the remaining default settings.
- Click Save.
- Navigate to Applications > Applications >
<App>> Okta API Scopes. - Search for
okta.myAccount.webauthn.manage. - Select Grant if the scope is not already enabled.
