<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-M74D8PB" height="0" width="0" style="display:none;visibility:hidden">
Loading
Skip to NavigationSkip to Main Content

Register Passkeys in Android Applications Using the Okta MyAccounts API

Okta Identity Engine
SDKs & Libraries

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.

 

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.

  1. 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" />
  2. Edit the DynamicAuthViewModel file to specify the required acr_value of urn: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.
  3. 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"
    ...
  1. 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
    ...
  1. 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
    ...
  1. Edit the strings.xml file to add the text for the register passkey button.
    <string name="register_passkey">Register New Passkey</string>
  1. 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.manage scope allows the application to make MyAccount Web Authentication (WebAuthn) API calls.
  • The urn:okta:loa:2fa:any:ifpossible Authentication Context Class Reference (ACR) value steps up the session, allowing Okta to grant the okta.myAccount.webauthn.manage scope.
  • The DashboardViewModel modifications add the registerPasskey() and createPasskey() functions.
    • The registerPasskey() function starts the registration process by calling the MyAccount API startWebAuthnEnrollment.
    • The createPasskey() function uses androidx.credentials.CredentialManager to 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.

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.

  1. Navigate to Security > API > Authorization Servers > <Auth_Server> > Scopes in the Okta Admin Console.
  2. Click Add Scope.
  3. Enter okta.myAccount.webauthn.manage in the Name field.
  4. Enter an optional display name and description, and keep the remaining default settings.
  5. Click Save.
  6. Navigate to Applications > Applications > <App> > Okta API Scopes.
  7. Search for okta.myAccount.webauthn.manage.
  8. Select Grant if the scope is not already enabled.

 

Related References

Loading
Okta Support - Register Passkeys in Android Applications Using the Okta MyAccounts API