<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 with the MyAccounts API
Okta Identity Engine
SDKs & Libraries
Overview

This article details how to use the Okta MyAccounts API to register Passkeys with Android Applications.

Applies To
Cause

The Okta IDX pipeline supports registering Passkey Credentials during Authenticator Enrollment. At times, it may be desirable to allow Passkey registration outside of an authentication flow.

Solution

The code below builds on the okta-mobile-kotlin IDX Sample Application. Functionality is added to allow Passkey Enrollment once logged into the sample app.

The following Sample App files will be modified:

  • AndroidManifest.xml
  • DynamicAuthViewModel.kt
  • DashboardViewModel.kt
  • DashboardFragment.kt
  • fragment_dashboard.xml

A prerequisite is to have the sample application set up, along with an Org configured for the Android AssetLinks.json file. Please refer to Configure Passkeys for Native Logins in Android Applications.

 

  1. Edit AndroidManifest.xml to allow internet access.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  1. Edit DynamicAuthViewModel to specify required acr_value 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.
...
  1. Edit SampleCredentialHelper 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 fragment_dashboard.xml 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 DashboardFragment to set up the View / 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 strings.xml to add the text for the register passkey button.
<string name="register_passkey">Register New Passkey</string>
  1. Edit DashboardViewModel, which will contain 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
    }
...

Summary of Changes

Scope okta.myAccount.webauthn.manage and ACR value urn:okta:loa:2fa:any:ifpossible were added.

  • okta.myAccount.webauthn.manage is needed for the MyAccount WebAuthn API calls.
  • urn:okta:loa:2fa:any:ifpossible is used to step up the session, so Okta will grant okta.myAccount.webauthn.manage.

NOTE: *All Okta MyAccount API must include an http 'Accept' header with the value application/json; okta-version=1.0.0 or Okta will reject the request.

The next few edits add the button and messaging to the Dashboard.

 

DashboardViewModel Modifications:

  • registerPasskey():
  • createPasskey():
    • With the Activation Data from Okta, use androidx.credentials.CredentialManager to initiate Credential enrollment
    • On successful creation, extracts ClientData and Attestation.
    • Finishes the registration process by calling the MyAccount API createWebAuthnEnrollment.

 

Required Okta Org Changes

In addition to the code changes, a couple of changes will need to be made to the OIDC Application and Authorization Server in Okta.

 

Authorization Server

  1. Navigate to Security > API > Authorization Servers ${Auth_Server} > Scopes.
  2. Click Add Scope.
  3. For "name", enter okta.myAccount.webauthn.manage.
  4. Optionally add a "Display name" / "Description" and keep the rest of the defaults.
  5. Click Save.

OIDC Application

  • Navigate to Applications > Applications > ${App} Okta API Scopes.
  • Search for okta.myAccount.webauthn.manage.
  • Select "Grant" if not already enabled.

 

Related References

Loading
Register Passkeys in Android with the MyAccounts API