<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[White Rock Studios Blog]]></title><description><![CDATA[White Rock Studios Blog]]></description><link>https://blog.whiterockstudios.com</link><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 16:45:11 GMT</lastBuildDate><atom:link href="https://blog.whiterockstudios.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Creating a Passkey Client for Android]]></title><description><![CDATA[With the backend in place, let’s shift to Android and implement the client side flow using Kotlin and Android Jetpack’s Credential Manager API. This article will guide you through integrating biometric prompts, handling passkey creation and sign in, ...]]></description><link>https://blog.whiterockstudios.com/creating-a-passkey-client-for-android</link><guid isPermaLink="true">https://blog.whiterockstudios.com/creating-a-passkey-client-for-android</guid><category><![CDATA[mobile auth]]></category><category><![CDATA[#webauthn]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Android]]></category><category><![CDATA[android apps]]></category><category><![CDATA[authentication]]></category><category><![CDATA[passkey ]]></category><category><![CDATA[passkeys]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[mobile app development]]></category><category><![CDATA[mobile security]]></category><dc:creator><![CDATA[Jeff Day]]></dc:creator><pubDate>Sun, 23 Mar 2025 02:46:28 GMT</pubDate><content:encoded><![CDATA[<p>With the backend in place, let’s shift to Android and implement the client side flow using Kotlin and Android Jetpack’s Credential Manager API. This article will guide you through integrating biometric prompts, handling passkey creation and sign in, and communicating with the server using WebAuthn compatible data. All while delivering a seamless user experience.</p>
<h2 id="heading-overview">Overview</h2>
<p>This project will create a client Android application that talks to a restful server to handle Passkey authentication. You will have to test on a physical Android device. CredentialManager will not work on an Android emulator.</p>
<h2 id="heading-setting-up-the-project">Setting up the Project</h2>
<p>Create a new project in Android Studio. I'm using Kotlin and Jetpack Compose for the UI. Jetpack Compose isn't required, but it made the job a lot easier for me. Also, I'm using retrofit for networking and <a target="_blank" href="https://github.com/square/moshi">Moshi</a> for serializing JSON to and from the server endpoints.</p>
<h3 id="heading-creating-a-simple-login-screen">Creating a Simple Login Screen</h3>
<p>Once you've created the bones of a project, we'll start with a basic login screen UI. I'm going to hand the reins off to Jetpack Compose from <code>MainActivity</code>, use a view model, and I'm also keeping an instance of my <code>PasskeyManager</code> class in the activity. More on that later.</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> : <span class="hljs-type">ComponentActivity</span></span>() {

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> viewModel: MainViewModel <span class="hljs-keyword">by</span> viewModels()
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> passkeyManager = PasskeyManager(<span class="hljs-keyword">this</span>)

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(savedInstanceState: <span class="hljs-type">Bundle</span>?)</span></span> {
        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState)

        enableEdgeToEdge()
        setContent {
            PasskeysAndroidTheme {
                PasskeyApp(viewModel)
            }
        }

        setupViewModel()
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setupViewModel</span><span class="hljs-params">()</span></span> {

    }
}

<span class="hljs-meta">@Preview(showBackground = true)</span>
<span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">MainPreview</span><span class="hljs-params">()</span></span> {
    PasskeysAndroidTheme {
        PasskeyApp(viewModel = MainViewModel())
    }
}
</code></pre>
<p>Later, we're going to use <code>setupViewModel</code> to implement a couple lamdbas in the view model for creating and validating passkeys. <code>PasskeyManager</code> holds an instance of Android's <code>CredentialManager</code> class that handles passkeys and it needs a context. It's a no-no to pass an actvity context into a view model, so I'm working around that this way. If you're a seasoned Android developer, you've probably got a solution in your back pocket that's better than mine. I've also got a preview here to show the UI for the only screen in this app.</p>
<p>Here's the start of the <code>MainViewModel</code> class. I'm also including a <code>LoginStatus</code> enum here. For this tutorial, I'm using this to keep track of where we are in the flow and to help with debugging. I'm also showing a snackbar for key events along the way.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">enum</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginStatus</span></span>(<span class="hljs-keyword">var</span> displayString: String) {
    SIGN_UP_IN_PROGRESS(<span class="hljs-string">"Sign up in progress..."</span>),
    SIGN_UP_SUCCESS(<span class="hljs-string">"Signed up successfully!"</span>),
    SIGN_UP_FAILED(<span class="hljs-string">"Sign up failed"</span>),
    LOGIN_IN_PROGRESS(<span class="hljs-string">"Logging in..."</span>),
    LOGIN_SUCCESS(<span class="hljs-string">"Logged in!"</span>),
    LOGIN_FAILED(<span class="hljs-string">"Login failed"</span>),
    LOGGED_OUT(<span class="hljs-string">"Logged out"</span>)
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainViewModel</span> : <span class="hljs-type">ViewModel</span></span>() {

    <span class="hljs-keyword">var</span> loginStatus = mutableStateOf(LoginStatus.LOGGED_OUT)
    <span class="hljs-keyword">var</span> username = mutableStateOf(<span class="hljs-string">""</span>)
    <span class="hljs-keyword">var</span> snackbarHostState = SnackbarHostState()

    <span class="hljs-keyword">var</span> createPasskey: (userId: String, payload: String) -&gt; <span class="hljs-built_in">Unit</span> = { _, _ -&gt; }
    <span class="hljs-keyword">var</span> validateCredential: (username: String, request: GetCredentialRequest) -&gt; <span class="hljs-built_in">Unit</span> = { _, _ -&gt; }

    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">showSnackbar</span><span class="hljs-params">(status: <span class="hljs-type">LoginStatus</span>, errorMessage: <span class="hljs-type">String</span>? = <span class="hljs-literal">null</span>)</span></span> {
        <span class="hljs-keyword">val</span> message = status.displayString + (errorMessage ?: <span class="hljs-string">""</span>)
        snackbarHostState.showSnackbar(message)
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> moshi: Moshi = Moshi.Builder()
        .add(KotlinJsonAdapterFactory())
        .build()
}
</code></pre>
<p>The <code>PasskeyApp</code> composable holds the Scaffold for the Jetpack Compose UI. It's pretty simple:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">PasskeyApp</span><span class="hljs-params">(
    viewModel: <span class="hljs-type">MainViewModel</span>
)</span></span> {
    <span class="hljs-keyword">val</span> coroutineScope = rememberCoroutineScope()

    Scaffold(
        snackbarHost = {
            SnackbarHost(hostState = viewModel.snackbarHostState)
        },
        modifier = Modifier.fillMaxSize()
    ) { innerPadding -&gt;
        LoginView(
            viewModel = viewModel,
            onLogin = {
                coroutineScope.launch {
                    viewModel.onLoginButtonTapped()
                }
            },
            onSignUp = {
                coroutineScope.launch {
                    viewModel.onSignUpButtonTapped()
                }
            },
            modifier = Modifier.padding(innerPadding)
        )
    }
}
</code></pre>
<h3 id="heading-setting-up-networking">Setting up Networking</h3>
<p>Like most Android projects, I'm using Retrofit for handling network requests. I have set up a pretty basic interface:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ApiInterface</span> </span>{
    <span class="hljs-keyword">companion</span> <span class="hljs-keyword">object</span> {
        <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> BASE_URL = <span class="hljs-string">"https://942e-2600-1700-3e41-88b0-147b-69e-2c32-ee33.ngrok-free.app"</span>

        <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">retrofit</span><span class="hljs-params">()</span></span>: Retrofit {
            <span class="hljs-keyword">val</span> moshi = Moshi.Builder().build()
            <span class="hljs-keyword">return</span> Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(MoshiConverterFactory.create(moshi))
                .client(okHttpClient())
                .build()
        }

        <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">okHttpClient</span><span class="hljs-params">()</span></span>: OkHttpClient {
            <span class="hljs-keyword">val</span> loggingInterceptor = HttpLoggingInterceptor()
            loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY

            <span class="hljs-keyword">return</span> OkHttpClient.Builder()
                .addInterceptor(DefaultHeaderInterceptor())
                .addInterceptor(loggingInterceptor)
                .build()
        }
    }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DefaultHeaderInterceptor</span></span>() : Interceptor {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">intercept</span><span class="hljs-params">(chain: <span class="hljs-type">Interceptor</span>.<span class="hljs-type">Chain</span>)</span></span>: okhttp3.Response {
        <span class="hljs-keyword">var</span> request = chain.request()
            .newBuilder()
            .addHeader(<span class="hljs-string">"Accept"</span>, <span class="hljs-string">"application/json"</span>)
            .addHeader(<span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">"application/json"</span>)
            .build()

        <span class="hljs-keyword">return</span> chain.proceed(request)
    }
}
</code></pre>
<p>Quick rundown on what's happening above, I'm building an instance of Retrofit and setting it up with Moshi. I'm also setting up an <code>OkHttpClient</code> with logging, as well as an interceptor that will add a set of headers to all of my requests. I'm also using a <code>BASE_URL</code> value that will change each time my ngrok address changes. For this tutorial, you're going to want something that allows you to connect to your local Passkey server from a physical Android device. ngrok can help with that.</p>
<h3 id="heading-a-quick-note-about-ngrok">A Quick Note about ngrok</h3>
<p><code>ngrok</code> is a tool that creates an http or https endpoint and routes the traffic to your local machine. You're going to need this to make calls to your local server during testing. You can download ngrok for free <a target="_blank" href="https://ngrok.com/downloads/mac-os">here</a>. Once you get it set up, all you have to do is run <code>./ngrok http 8080</code> in Terminal and you'll be able to connect to your local server with the URL it provides. The https version of the URL will be your relying party, and the base URL for connecting to your backend server. Don't forget to update those values if you turn ngrok off and back on again, because the URL will change.</p>
<h3 id="heading-setting-up-a-few-kotlin-extensions">Setting up a Few Kotlin Extensions</h3>
<p>Since we'll do lots of base 64 url safe encoding, here's an extension I'm using in several places:</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> String.<span class="hljs-title">base64UrlEncoded</span><span class="hljs-params">()</span></span>: String {
    <span class="hljs-keyword">return</span> Base64.encodeToString(
        <span class="hljs-keyword">this</span>.encodeToByteArray(),
        Base64.NO_WRAP or Base64.URL_SAFE or Base64.NO_PADDING
    )
}
</code></pre>
<p>I've also got these Moshi functions for converting to and from JSON:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// [Moshi] extension to transform an object to json</span>
<span class="hljs-keyword">inline</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-type">&lt;<span class="hljs-keyword">reified</span> T&gt;</span> Moshi.<span class="hljs-title">objectToJson</span><span class="hljs-params">(<span class="hljs-keyword">data</span>: <span class="hljs-type">T</span>)</span></span>: String =
    adapter(T::<span class="hljs-keyword">class</span>.java).toJson(<span class="hljs-keyword">data</span>)

<span class="hljs-comment">// [Moshi] extension to transform json to an object</span>
<span class="hljs-keyword">inline</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-type">&lt;<span class="hljs-keyword">reified</span> T&gt;</span> Moshi.<span class="hljs-title">jsonToObject</span><span class="hljs-params">(json: <span class="hljs-type">String</span>)</span></span>: T? =
    adapter(T::<span class="hljs-keyword">class</span>.java).fromJson(json)
</code></pre>
<h2 id="heading-passkeymanager-and-credentialmanager">PasskeyManager and CredentialManager</h2>
<p>This project uses Android's <code>CredentialManager</code> for handling the Passkey flow. CredentialManager requires an activity context. I'm trying to have some separation of concerns and not put all of this Passkey handling code in the activity, so I created a <code>PasskeyManager</code> class to encapsulate it in one place.</p>
<p>Here's a basic outline of the PasskeyManager class:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PasskeyManager</span></span>(<span class="hljs-keyword">val</span> activity: ComponentActivity) {

    <span class="hljs-keyword">var</span> credentialManager = CredentialManager.create(context = activity)

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> moshi: Moshi = Moshi.Builder()
        .add(KotlinJsonAdapterFactory())
        .build()

    <span class="hljs-meta">@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)</span>
    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">createPasskey</span><span class="hljs-params">(
        userId: <span class="hljs-type">String</span>,
        requestJson: <span class="hljs-type">String</span>,
        preferImmediatelyAvailableCredentials: <span class="hljs-type">Boolean</span>
    )</span></span>: <span class="hljs-built_in">Boolean</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
    }
}

<span class="hljs-keyword">private</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">handlePasskeyRegistrationResult</span><span class="hljs-params">(
        userId: <span class="hljs-type">String</span>, 
        result: <span class="hljs-type">CreateCredentialResponse</span>
    )</span></span>: <span class="hljs-built_in">Boolean</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
}

<span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">validateCredential</span><span class="hljs-params">(
        username: <span class="hljs-type">String</span>,
        request: <span class="hljs-type">GetCredentialRequest</span>
    )</span></span>: <span class="hljs-built_in">Boolean</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
}

<span class="hljs-keyword">private</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">handleSignInResult</span><span class="hljs-params">(
        username: <span class="hljs-type">String</span>,
        result: <span class="hljs-type">GetCredentialResponse</span>
    )</span></span>: <span class="hljs-built_in">Boolean</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
}
</code></pre>
<h3 id="heading-creating-a-passkey">Creating a Passkey</h3>
<p>Before we can log in with a Passkey on our Android device, we've got to create one. If you're building on my <a target="_blank" href="https://whiterockstudios.hashnode.dev/creating-a-passkey-server-for-ios-and-android-in-swift">vapor Passkey server article</a>, you can enter any username or email address or whatever into the <code>TextField</code> and tap the <code>Sign Up</code> button. That's going to call <code>onSignUpButtonTapped</code> in our view model. Here's the implementation for that. The first thing that has to happen is making a call to our Passkey server.</p>
<p>I'll show the whole <code>LoginAPI</code> now, but for now we're focused on the <code>signUp</code> function. It makes a call to the <code>/signup</code> endpoint and passes a <code>username</code> param from our text field:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">LoginApi</span> </span>{
    <span class="hljs-meta">@GET(<span class="hljs-meta-string">"/signup"</span>)</span>
    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">signUp</span><span class="hljs-params">(
        <span class="hljs-meta">@Query(<span class="hljs-meta-string">"username"</span>)</span> username: <span class="hljs-type">String</span>
    )</span></span> : Response&lt;ChallengeResponse&gt;

    <span class="hljs-meta">@POST(<span class="hljs-meta-string">"/makeCredential"</span>)</span>
    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">makeCredential</span><span class="hljs-params">(
        <span class="hljs-meta">@Query(<span class="hljs-meta-string">"userId"</span>)</span> userId: <span class="hljs-type">String</span>,
        <span class="hljs-meta">@Body</span> postBody: <span class="hljs-type">MakeCredentialPostBody</span>
    )</span></span> : Response&lt;<span class="hljs-built_in">Unit</span>&gt;

    <span class="hljs-meta">@GET(<span class="hljs-meta-string">"/authenticate"</span>)</span>
    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">login</span><span class="hljs-params">(
        <span class="hljs-meta">@Query(<span class="hljs-meta-string">"username"</span>)</span> username: <span class="hljs-type">String</span>
    )</span></span> : Response&lt;PublicKeyCredentialRequestOptions&gt;

    <span class="hljs-meta">@POST(<span class="hljs-meta-string">"/authenticate"</span>)</span>
    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">authenticate</span><span class="hljs-params">(
        <span class="hljs-meta">@Query(<span class="hljs-meta-string">"username"</span>)</span> username: <span class="hljs-type">String</span>,
        <span class="hljs-meta">@Body</span> postBody: <span class="hljs-type">PublicKeyCredentialBody</span>
    )</span></span> : Response&lt;<span class="hljs-built_in">Unit</span>&gt;

    <span class="hljs-keyword">companion</span> <span class="hljs-keyword">object</span> {
        <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">retrofit</span><span class="hljs-params">()</span></span>: LoginApi {
            <span class="hljs-keyword">return</span> ApiInterface.retrofit().create(LoginApi::<span class="hljs-keyword">class</span>.java)
        }
    }
}
</code></pre>
<p>Here's the implementation of <code>onSignUpButtonTapped</code> in the view model:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onSignUpButtonTapped</span><span class="hljs-params">()</span></span> {
    showSnackbar(LoginStatus.SIGN_UP_IN_PROGRESS)

    <span class="hljs-comment">// 1. Make a call to the sign up endpoint</span>
    <span class="hljs-keyword">val</span> response = LoginApi.retrofit().signUp(username.value)


    <span class="hljs-keyword">if</span> (response.isSuccessful) {
        Log.i(<span class="hljs-string">""</span>, <span class="hljs-string">"got a response: <span class="hljs-variable">$response</span>"</span>)

        response.body()?.let { challengeResponse -&gt;

            <span class="hljs-comment">// 2. Base64 url encode the userId and challenge</span>
            <span class="hljs-keyword">val</span> userIdEncoded = challengeResponse.userId.base64UrlEncoded()
            <span class="hljs-keyword">val</span> challengeEncoded = challengeResponse.challenge.challenge.base64UrlEncoded()

            <span class="hljs-keyword">val</span> user = challengeResponse.challenge.user
            user.id = userIdEncoded

            <span class="hljs-comment">// 3. Build a PublicKeyCredentialCreationOptions and convert it to JSON</span>
            <span class="hljs-keyword">val</span> creationOptions = PublicKeyCredentialCreationOptions(
                rp = challengeResponse.challenge.relyingParty,
                user = user,
                challenge = challengeEncoded,
                pubKeyCredParams = challengeResponse.challenge.params,
                timeout = challengeResponse.challenge.timeout,
                attestation = challengeResponse.challenge.attestation,
                excludeCredentials = listOf()
            )

            <span class="hljs-keyword">val</span> json = moshi.objectToJson(creationOptions)
            Log.i(<span class="hljs-string">""</span>, <span class="hljs-string">"creationOptions json: <span class="hljs-variable">$json</span>"</span>)

            <span class="hljs-comment">// 4. Ready to create our Passkey using Android's CredentialManager</span>
            createPasskey(
                challengeResponse.userId,
                moshi.objectToJson(creationOptions)
            )
        } ?: run {
            showSnackbar(LoginStatus.SIGN_UP_FAILED)
        }
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">val</span> errorMessage = response.errorBody().toString()
        Log.e(<span class="hljs-string">""</span>, <span class="hljs-string">"got an error: <span class="hljs-variable">$errorMessage</span>"</span>)
        showSnackbar(
            status = LoginStatus.SIGN_UP_FAILED,
            errorMessage = errorMessage
        )
    }
}
</code></pre>
<p>Here's a breakdown of what's happening above. It looks like a lot, but it's not that bad:</p>
<ol>
<li><p>As mentioned above, we make a call to our server's sign up endpoint. This will create our new user in the system (if it doesn't already exist), and send back a challenge.</p>
</li>
<li><p>We need to Base64 url encode the challenge. This is a common theme throughout a Passkey implementation. Everything gets Base64 url encoded.</p>
</li>
<li><p>We build up a <code>PublicKeyCredentialCreationOptions</code>, which holds the details that are needed to create our Passkey. We're also using Moshi to convert this object to JSON.</p>
</li>
<li><p>The JSON object, along with our user's id, gets sent off to the <code>createPasskey</code> function, which will use <code>CredentialManager</code> to build a Passkey for Android.</p>
</li>
</ol>
<p>Here's the implementation for <code>PublicKeyCredentialCreationOptions</code>:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@JsonClass(generateAdapter = true)</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PublicKeyCredentialCreationOptions</span></span>(
    <span class="hljs-keyword">val</span> rp: RelyingParty,
    <span class="hljs-keyword">val</span> user: AuthUser,
    <span class="hljs-keyword">val</span> challenge: String,
    <span class="hljs-keyword">val</span> pubKeyCredParams: List&lt;PublicKeyCredParams&gt;,
    <span class="hljs-keyword">val</span> timeout: <span class="hljs-built_in">Int</span>,
    <span class="hljs-keyword">val</span> attestation: String,
    <span class="hljs-keyword">val</span> excludeCredentials: List&lt;String&gt;
)
</code></pre>
<p>Earlier, we created a <code>setupViewModel</code> function in MainActivity. It's time to add our implementation for <code>createPasskey</code> there, so the view model can call it:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setupViewModel</span><span class="hljs-params">()</span></span> {
    viewModel.createPasskey = { userId, payload -&gt;
        lifecycleScope.launch {
            <span class="hljs-keyword">val</span> result = passkeyManager.createPasskey(
                userId = userId,
                requestJson = payload,
                preferImmediatelyAvailableCredentials = <span class="hljs-literal">true</span>
            )

            <span class="hljs-keyword">if</span> (result) {
                viewModel.showSnackbar(LoginStatus.SIGN_UP_SUCCESS)
            } <span class="hljs-keyword">else</span> {
                viewModel.showSnackbar(LoginStatus.SIGN_UP_FAILED)
            }
        }
    }
}
</code></pre>
<p>We're really only doing this here because <code>CredentialManager</code> needs a context. Trying to have some kind of encapsulation, I'm using a <code>PasskeyManager</code> class to hold an instance of CredentialManager and the code for managing Passkey registration and authentication. Here's our <code>createPasskey</code> implementation:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)</span>
<span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">createPasskey</span><span class="hljs-params">(
    userId: <span class="hljs-type">String</span>,
    requestJson: <span class="hljs-type">String</span>,
    preferImmediatelyAvailableCredentials: <span class="hljs-type">Boolean</span>
)</span></span>: <span class="hljs-built_in">Boolean</span> {
    Log.i(<span class="hljs-string">""</span>, <span class="hljs-string">"ready to create public key credentials with: <span class="hljs-variable">$requestJson</span>"</span>)

    <span class="hljs-keyword">val</span> createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest(
        <span class="hljs-comment">// Contains the request in JSON format. Uses the standard WebAuthn web JSON spec.</span>
        requestJson = requestJson,
        <span class="hljs-comment">// Defines whether you prefer to use only immediately available credentials,</span>
        <span class="hljs-comment">// not hybrid credentials, to fulfill this request. This value is false by default.</span>
        preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
    )

    <span class="hljs-comment">// Execute CreateCredentialRequest asynchronously to register credentials</span>
    <span class="hljs-comment">// for a user account. Handle success and failure cases with the result and</span>
    <span class="hljs-comment">// exceptions, respectively.</span>
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">val</span> result = credentialManager.createCredential(
            context = activity,
            request = createPublicKeyCredentialRequest
        )

        <span class="hljs-keyword">return</span> handlePasskeyRegistrationResult(userId, result)
    } <span class="hljs-keyword">catch</span> (e : CreateCredentialException) {
        Log.e(<span class="hljs-string">""</span>, <span class="hljs-string">"failed to create a credential: <span class="hljs-variable">$e</span>"</span>)

        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
    }
}
</code></pre>
<p>At this point, the Android OS will bring up a dialog to create a Passkey and gives back a result. Let's handle that:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">private</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">handlePasskeyRegistrationResult</span><span class="hljs-params">(
    userId: <span class="hljs-type">String</span>,
    result: <span class="hljs-type">CreateCredentialResponse</span>
)</span></span>: <span class="hljs-built_in">Boolean</span> {
    (result <span class="hljs-keyword">as</span>? CreatePublicKeyCredentialResponse)?.let {

        <span class="hljs-comment">// 1. Use Moshi to decode the registration response</span>
        <span class="hljs-keyword">val</span> createCredentialObject = moshi.jsonToObject&lt;CreateCredential&gt;(it.registrationResponseJson)
        Log.i(<span class="hljs-string">""</span>, <span class="hljs-string">"here's the registrationResponseJson: <span class="hljs-subst">${it.registrationResponseJson}</span>"</span>)

        <span class="hljs-comment">// 2. Generate a JSON payload to send the Passkey to the server</span>
        createCredentialObject?.payload()?.let { payload -&gt;
            Log.i(<span class="hljs-string">""</span>, <span class="hljs-string">"credentialPayload: <span class="hljs-variable">$payload</span>"</span>)

            <span class="hljs-keyword">val</span> response = LoginApi.retrofit().makeCredential(userId = userId, postBody = payload)
            <span class="hljs-keyword">if</span> (response.isSuccessful) {
                Log.i(<span class="hljs-string">""</span>, <span class="hljs-string">"got makeCredential response: <span class="hljs-variable">$response</span>"</span>)
                <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
            } <span class="hljs-keyword">else</span> {
                <span class="hljs-keyword">val</span> error = response.errorBody().toString()
                Log.e(<span class="hljs-string">""</span>, <span class="hljs-string">"got an error: <span class="hljs-variable">$error</span>"</span>)
                <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
            }
        } ?: run {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
        }
    } ?: run {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
    }
}
</code></pre>
<h4 id="heading-buckle-up-its-about-to-get-weird">Buckle Up, It's About to Get Weird</h4>
<p>I'm going to share the <code>CreateCredential</code> class and its <code>payload</code> function next. The authenticator data that we need to pass along contains a byte array with binary data and a number of flags. Worlds collide here, as Android is not quite following the rules on the format of these flags, and the Swift library is not flexible in handling that. So we've got to tap into the authenticator data before it gets sent and make sure everything lines up. In my experience, the Android-generated data says it's including extension data (the ED flag), but doesn't actually include any. Android also includes attestation data, but doesn't set the AT flag. The WebAuthn library isn’t having that, and the whole thing fails.</p>
<p>I don't think I would have figured this part out without ChatGPT, and probably would have given up. I'll share what I came up with that works, and feel free to use, edit, and dig into it on your own:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@JsonClass(generateAdapter = true)</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CreateCredential</span></span>(
    <span class="hljs-keyword">val</span> id: String,
    <span class="hljs-keyword">val</span> rawId: String,
    <span class="hljs-keyword">val</span> authenticatorAttachment: String,
    <span class="hljs-keyword">val</span> type: String,
    <span class="hljs-keyword">val</span> response: CreateCredentialInnerResponse
) {
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">payload</span><span class="hljs-params">()</span></span>: MakeCredentialPostBody {
        <span class="hljs-keyword">val</span> cborMap = decodedCBOR
        <span class="hljs-keyword">var</span> authenticatorData = cborMap[<span class="hljs-string">"authData"</span>].GetByteString().clone()

        <span class="hljs-keyword">val</span> flags = authenticatorData[<span class="hljs-number">32</span>].toInt() and <span class="hljs-number">0xFF</span>
        Log.d(<span class="hljs-string">"WebAuthn"</span>, <span class="hljs-string">"Original Flags Byte: 0x<span class="hljs-subst">${flags.toString(<span class="hljs-number">16</span>).uppercase()}</span> (Binary: <span class="hljs-subst">${flags.toString(<span class="hljs-number">2</span>).padStart(<span class="hljs-number">8</span>, <span class="hljs-string">'0'</span>)}</span>)"</span>)
        Log.d(<span class="hljs-string">"WebAuthn"</span>, <span class="hljs-string">"Original Authenticator Data Length: <span class="hljs-subst">${authenticatorData.size}</span>"</span>)

        <span class="hljs-comment">// ✅ Always clear ED (`0x80`)</span>
        authenticatorData[<span class="hljs-number">32</span>] = (authenticatorData[<span class="hljs-number">32</span>].toInt() and <span class="hljs-number">0x7F</span>).toByte() <span class="hljs-comment">// `0x7F` = `01111111`</span>

        <span class="hljs-comment">// ✅ If extra bytes exist (authenticatorData &gt; 37), set `AT = true`</span>
        <span class="hljs-keyword">if</span> (authenticatorData.size &gt; <span class="hljs-number">37</span>) {
            authenticatorData[<span class="hljs-number">32</span>] = (authenticatorData[<span class="hljs-number">32</span>].toInt() or <span class="hljs-number">0x40</span>).toByte() <span class="hljs-comment">// `0x40` = `01000000`</span>
        }

        <span class="hljs-keyword">val</span> fixedFlags = authenticatorData[<span class="hljs-number">32</span>].toInt() and <span class="hljs-number">0xFF</span>
        Log.d(<span class="hljs-string">"WebAuthn"</span>, <span class="hljs-string">"Fixed Flags Byte: 0x<span class="hljs-subst">${fixedFlags.toString(<span class="hljs-number">16</span>).uppercase()}</span> (Binary: <span class="hljs-subst">${fixedFlags.toString(<span class="hljs-number">2</span>).padStart(<span class="hljs-number">8</span>, <span class="hljs-string">'0'</span>)}</span>)"</span>)
        Log.d(<span class="hljs-string">"WebAuthn"</span>, <span class="hljs-string">"Final Authenticator Data Length: <span class="hljs-subst">${authenticatorData.size}</span>"</span>)

        <span class="hljs-keyword">val</span> modifiedCborMap = CBORObject.NewMap()
        modifiedCborMap[<span class="hljs-string">"fmt"</span>] = cborMap[<span class="hljs-string">"fmt"</span>]
        modifiedCborMap[<span class="hljs-string">"attStmt"</span>] = cborMap[<span class="hljs-string">"attStmt"</span>]
        modifiedCborMap[<span class="hljs-string">"authData"</span>] = CBORObject.FromObject(authenticatorData)

        <span class="hljs-keyword">val</span> modifiedCborBytes = modifiedCborMap.EncodeToBytes()
        <span class="hljs-keyword">val</span> newAttestationObject = Base64.encodeToString(modifiedCborBytes, Base64.NO_WRAP or Base64.URL_SAFE or Base64.NO_PADDING)

        <span class="hljs-keyword">val</span> credentialPayload = MakeCredentialPostBody(
            id = id,
            rawId = rawId,
            authenticatorAttachment = authenticatorAttachment,
            type = type,
            response = MakeCredentialResponseParam(
                attestationObject = newAttestationObject,
                clientDataJSON = response.clientDataJSON
            )
        )

        <span class="hljs-keyword">return</span> credentialPayload
    }
}

<span class="hljs-meta">@JsonClass(generateAdapter = true)</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CreateCredentialInnerResponse</span></span>(
    <span class="hljs-keyword">val</span> attestationObject: String,
    <span class="hljs-keyword">val</span> clientDataJSON: String
)

<span class="hljs-meta">@JsonClass(generateAdapter = true)</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MakeCredentialPostBody</span></span>(
    <span class="hljs-keyword">val</span> id: String,
    <span class="hljs-keyword">val</span> rawId: String,
    <span class="hljs-keyword">val</span> authenticatorAttachment: String,
    <span class="hljs-keyword">val</span> type: String,
    <span class="hljs-keyword">val</span> response: MakeCredentialResponseParam
)

<span class="hljs-meta">@JsonClass(generateAdapter = true)</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MakeCredentialResponseParam</span></span>(
    <span class="hljs-keyword">val</span> attestationObject: String,
    <span class="hljs-keyword">val</span> clientDataJSON: String
)
</code></pre>
<p>This code basically takes the credential data apart and puts it back together as JSON, for sending to the Passkey server.</p>
<h4 id="heading-back-to-passkeymanager">Back to PasskeyManager</h4>
<p>Back in PasskeyManager's <code>handlePasskeyRegistrationResult</code>, we have to send this payload to our server's <code>/makeCredential</code> endpoint. If that succeeds, we've successfully created a Passkey, saved it on the server, and it can be used to log in.</p>
<h2 id="heading-signing-in">Signing In</h2>
<p>When the user taps the <code>Log in</code> button, we'll trigger onLoginButtonTapped in the view model:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onLoginButtonTapped</span><span class="hljs-params">()</span></span> {
    showSnackbar(LoginStatus.LOGIN_IN_PROGRESS)

    <span class="hljs-keyword">val</span> username = username.value

    <span class="hljs-comment">// 1. Call our login endpoint</span>
    <span class="hljs-keyword">val</span> response = LoginApi.retrofit().login(username)
    <span class="hljs-keyword">if</span> (response.isSuccessful) {
        Log.i(<span class="hljs-string">""</span>, <span class="hljs-string">"got an auth response: <span class="hljs-variable">$response</span>"</span>)

        response.body()?.let { credentialOptions -&gt;
            <span class="hljs-comment">// 2. Parse the response and prepare for sending to CredentialManager</span>
            <span class="hljs-keyword">val</span> responseJson = moshi.objectToJson(credentialOptions)
            <span class="hljs-keyword">val</span> jsonObject = JSONObject(responseJson)
            <span class="hljs-keyword">val</span> challenge = jsonObject.getString(<span class="hljs-string">"challenge"</span>)
            jsonObject.put(<span class="hljs-string">"challenge"</span>, challenge.base64UrlEncoded())

            <span class="hljs-keyword">val</span> newRequestJson = jsonObject.toString()
            <span class="hljs-keyword">val</span> getPublicKeyCredentialOption = GetPublicKeyCredentialOption(
                requestJson = newRequestJson
            )

            <span class="hljs-keyword">val</span> getPasswordOption = GetPasswordOption()
            <span class="hljs-keyword">val</span> credentialRequest = GetCredentialRequest(
                listOf(getPasswordOption, getPublicKeyCredentialOption)
            )

            <span class="hljs-comment">// 3. Use CredentialManager to validate the credentials</span>
            validateCredential(username, credentialRequest)
        } ?: run {
            Log.e(<span class="hljs-string">""</span>, <span class="hljs-string">"something went wrong at login"</span>)
            showSnackbar(status = LoginStatus.LOGIN_FAILED)
        }
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">val</span> errorMessage = response.errorBody().toString()
        <span class="hljs-keyword">val</span> responseCode = response.code()
        <span class="hljs-keyword">val</span> displayMessage = <span class="hljs-string">"got a <span class="hljs-variable">$responseCode</span>, error: <span class="hljs-variable">$errorMessage</span>"</span>

        Log.e(<span class="hljs-string">""</span>, <span class="hljs-string">"got a <span class="hljs-variable">$responseCode</span>,  error: <span class="hljs-variable">$errorMessage</span>"</span>)
        showSnackbar(
            status = LoginStatus.LOGIN_FAILED,
            errorMessage = displayMessage
        )
    }
}
</code></pre>
<p>Here's what this code does:</p>
<ol>
<li><p>Makes a call to the server's <code>/authenticate</code> endpoint, sending the username. The server looks up to see if that user exists, and sends back a challenge.</p>
</li>
<li><p>We need to Base64 url encode the challenge, and put that JSON into a <code>GetPublicKeyCredentialOption</code> class. This is a class Android provides. We then stick that into a <code>GetCredentialRequest</code>.</p>
</li>
<li><p>And off to <code>CredentialManager</code> it goes for validation.</p>
</li>
</ol>
<p>Back in MainActivity, we need to provide our implementation of <code>validateCredential</code> to the view model. This gives us a path to interact with PasskeyManager. So add this to <code>setupViewModels</code>:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setupViewModel</span><span class="hljs-params">()</span></span> {

    ...

    viewModel.validateCredential = { username, request -&gt;
        lifecycleScope.launch {
            <span class="hljs-keyword">val</span> result = passkeyManager.validateCredential(
                username = username,
                request = request
            )

            <span class="hljs-keyword">if</span> (result) {
                viewModel.showSnackbar(LoginStatus.LOGIN_SUCCESS)
            } <span class="hljs-keyword">else</span> {
                viewModel.showSnackbar(LoginStatus.LOGIN_FAILED)
            }
        }
    }
}
</code></pre>
<p>Now we can implement PasskeyManager's <code>validateCredential</code> function:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">validateCredential</span><span class="hljs-params">(
    username: <span class="hljs-type">String</span>,
    request: <span class="hljs-type">GetCredentialRequest</span>
)</span></span>: <span class="hljs-built_in">Boolean</span> {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">val</span> result = credentialManager.getCredential(
            context = activity,
            request = request
        )
        <span class="hljs-keyword">return</span> handleSignInResult(username, result)
    } <span class="hljs-keyword">catch</span> (e: GetCredentialException) {
        Log.e(<span class="hljs-string">""</span>, <span class="hljs-string">"got a credential exception: <span class="hljs-variable">$e</span>"</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
    }
}

<span class="hljs-keyword">private</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">handleSignInResult</span><span class="hljs-params">(
    username: <span class="hljs-type">String</span>,
    result: <span class="hljs-type">GetCredentialResponse</span>
)</span></span>: <span class="hljs-built_in">Boolean</span> {
    <span class="hljs-keyword">val</span> credential = result.credential

    <span class="hljs-keyword">when</span> (credential) {
        <span class="hljs-keyword">is</span> PublicKeyCredential -&gt; {
            <span class="hljs-keyword">val</span> responseJson = credential.authenticationResponseJson
            Log.i(<span class="hljs-string">""</span>, <span class="hljs-string">"got responseJson: <span class="hljs-variable">$responseJson</span>"</span>)
            <span class="hljs-keyword">val</span> createCredentialObject = moshi.jsonToObject&lt;PublicKeyCredentialBody&gt;(responseJson)?.let {
                <span class="hljs-keyword">val</span> response = LoginApi.retrofit().authenticate(username, it.payload)
                <span class="hljs-keyword">if</span> (response.isSuccessful) {
                    Log.i(<span class="hljs-string">""</span>, <span class="hljs-string">"authenticate response: <span class="hljs-variable">$response</span>"</span>)
                    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
                } <span class="hljs-keyword">else</span> {
                    <span class="hljs-keyword">val</span> error = response.errorBody().toString()
                    Log.e(<span class="hljs-string">""</span>, <span class="hljs-string">"got an error: <span class="hljs-variable">$error</span>"</span>)
                    <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> could throw an error?</span>
                    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
                }
            }
        }
        <span class="hljs-keyword">is</span> PasswordCredential -&gt; {
            <span class="hljs-keyword">val</span> username = credential.id
            <span class="hljs-keyword">val</span> password = credential.password
            Log.i(<span class="hljs-string">""</span>, <span class="hljs-string">"got a PasswordCredential"</span>)
            <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
        }
        <span class="hljs-keyword">else</span> -&gt; {
            <span class="hljs-comment">// Catch any unrecognized credential type here.</span>
            Log.e(<span class="hljs-string">""</span>, <span class="hljs-string">"Unexpected type of credential"</span>)
            <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
        }
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
}
</code></pre>
<p>Above, we make our call to CredentialManager's <code>getCredential</code>, and then handle the result. We really only care about the public key credential for the purposes of this project since we're concerned with Passkeys. From the user's perspective, they'll receieve a system prompt for biometrics, and if everything goes well you'll get a success and credential data.</p>
<p>We build up a POST payload from the credential, and send that to our <code>/authenticate</code> endpoint that receives a POST body.</p>
<p>Here's the implementation for <code>PublicKeyCredentialBody</code>:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@JsonClass(generateAdapter = true)</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PublicKeyCredentialBody</span></span>(
    <span class="hljs-keyword">val</span> id: String,
    <span class="hljs-keyword">val</span> rawId: String,
    <span class="hljs-keyword">val</span> type: String,
    <span class="hljs-keyword">val</span> authenticatorAttachment: String,
    <span class="hljs-keyword">val</span> response: PublicKeyCredentialResponse
) {
    <span class="hljs-keyword">val</span> payload: PublicKeyCredentialBody
        <span class="hljs-keyword">get</span>() {
            <span class="hljs-keyword">val</span> something = response.decodedAuthenticatorData
            Log.i(<span class="hljs-string">""</span>, <span class="hljs-string">"what actually gets sent: <span class="hljs-subst">${response.authenticatorData.unsignedBase64Encoded()}</span>"</span>)
            <span class="hljs-keyword">return</span> PublicKeyCredentialBody(
                id = id,
                rawId = rawId,
                type = type,
                authenticatorAttachment = authenticatorAttachment,
                response = PublicKeyCredentialResponse(
                    clientDataJSON = response.clientDataJSON,
                    authenticatorData = response.authenticatorData,
                    userHandle = response.userHandle.base64UrlEncoded(),
                    signature = response.signature
                )
            )
        }
}

<span class="hljs-meta">@JsonClass(generateAdapter = true)</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PublicKeyCredentialResponse</span></span>(
    <span class="hljs-keyword">val</span> clientDataJSON: String,
    <span class="hljs-keyword">val</span> authenticatorData: String,
    <span class="hljs-keyword">val</span> userHandle: String,
    <span class="hljs-keyword">val</span> signature: String
)
</code></pre>
<p>You should get a 200 success back from the server. If so, the user has successfully logged in. In a real application, you'll want to give the user a token at this point. They'll include that token in subsequent calls to your backend endpoints. I'll leave the rest of that exercise up to you.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>If you've made it this far, congrats! You're ready to fire up ngrok in a terminal, use the generated url as your relying party on your vapor server and Android application, and test it out!</p>
<p>If you got here first, here are the other 2 articles in this series:</p>
<ul>
<li><p><a target="_blank" href="https://whiterockstudios.hashnode.dev/creating-a-passkey-server-for-ios-and-android-in-swift">Build a WebAuthn Passkey server in Swift with Vapor</a></p>
</li>
<li><p><a target="_blank" href="https://whiterockstudios.hashnode.dev/creating-a-passkey-client-for-ios">Build an iOS client application that uses Passkeys</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Creating a Passkey Client for iOS]]></title><description><![CDATA[Now that we’ve set up our passkey server, it’s time to bring passwordless authentication to life on the client side, starting with iOS. In this article, we’ll walk through how to create a passkey enabled login experience using Swift, Face ID/Touch ID...]]></description><link>https://blog.whiterockstudios.com/creating-a-passkey-client-for-ios</link><guid isPermaLink="true">https://blog.whiterockstudios.com/creating-a-passkey-client-for-ios</guid><category><![CDATA[mobile login]]></category><category><![CDATA[passkeys]]></category><category><![CDATA[passkey ]]></category><category><![CDATA[#webauthn]]></category><category><![CDATA[Swift]]></category><category><![CDATA[vapor]]></category><category><![CDATA[iOS]]></category><category><![CDATA[Security]]></category><category><![CDATA[login]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[Mobile apps]]></category><category><![CDATA[mobile app development]]></category><category><![CDATA[mobile]]></category><category><![CDATA[biometric authentication]]></category><category><![CDATA[biometrics]]></category><dc:creator><![CDATA[Jeff Day]]></dc:creator><pubDate>Sat, 22 Mar 2025 20:09:40 GMT</pubDate><content:encoded><![CDATA[<p>Now that we’ve set up our passkey server, it’s time to bring passwordless authentication to life on the client side, starting with iOS. In this article, we’ll walk through how to create a passkey enabled login experience using Swift, Face ID/Touch ID, and Apple’s AuthenticationServices framework. You’ll learn how to register, authenticate, and handle credentials securely. No passwords required.</p>
<h2 id="heading-overview">Overview</h2>
<p>This project will create a client iOS application that talks to a restful server to handle Passkey authentication.</p>
<h2 id="heading-setting-up-the-project">Setting up the Project</h2>
<p>Create a new Xcode project. When prompted, you can use either SwiftUI or UIKit. My UI code example is SwiftUI for simplicity, but the meat of this article is the Passkey related code, and that will work great with either UI framework.</p>
<h3 id="heading-creating-a-simple-login-screen">Creating a Simple Login Screen</h3>
<p>Once you've created the bones of a project, we'll start with a basic login screen UI. This isn't the best it could be, but gets the job done:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">import</span> SwiftUI

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">LoginView</span>: <span class="hljs-title">View</span> </span>{

    @<span class="hljs-type">StateObject</span> <span class="hljs-keyword">var</span> authService = <span class="hljs-type">AuthService</span>()

    @<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> emailAddress = <span class="hljs-string">""</span>
    @<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> loginStatus = <span class="hljs-string">""</span>
    @<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> isLoading = <span class="hljs-literal">false</span>
    @<span class="hljs-type">FocusState</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> isTextFieldFocused: <span class="hljs-type">Bool</span>

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">ZStack</span> {
            <span class="hljs-type">VStack</span> {

                <span class="hljs-type">Spacer</span>()

                <span class="hljs-type">TextField</span>(<span class="hljs-string">"Email Address"</span>, text: $emailAddress)
                    .focused($isTextFieldFocused)
                    .autocorrectionDisabled()
                    .autocapitalization(.<span class="hljs-keyword">none</span>)
                    .textContentType(.emailAddress)
                    .onSubmit {
                        isTextFieldFocused = <span class="hljs-literal">false</span>
                    }
                    .padding()
                    .frame(maxWidth: .infinity)
                    .frame(height: <span class="hljs-number">44</span>)
                    .border(.separator, width: <span class="hljs-number">1</span>)
                    .padding()

                <span class="hljs-type">Text</span>(authService.authStatus.displayMessage)

                <span class="hljs-type">Spacer</span>()

                <span class="hljs-type">VStack</span>(spacing: <span class="hljs-number">20</span>) {
                    <span class="hljs-type">Button</span> {
                        signUp()
                    } label: {
                        <span class="hljs-type">Text</span>(<span class="hljs-string">"Sign Up"</span>)
                    }
                    .frame(maxWidth: .infinity)
                    .frame(height: <span class="hljs-number">50</span>)
                    .foregroundColor(.white)
                    .background(.blue)
                    .padding(.horizontal)

                    <span class="hljs-type">Button</span> {
                        login()
                    } label: {
                        <span class="hljs-type">Text</span>(<span class="hljs-string">"Log In"</span>)
                    }
                    .frame(maxWidth: .infinity)
                    .frame(height: <span class="hljs-number">50</span>)
                    .foregroundColor(.white)
                    .background(<span class="hljs-type">Color</span>(.systemCyan))
                    .padding(.horizontal)
                    .padding(.bottom)
                }
            }

            <span class="hljs-keyword">if</span> isLoading {
                <span class="hljs-type">ProgressView</span>()
            }
        }
        .alert(authService.authErrorTitle,
               isPresented: $authService.isShowingAuthError, actions: {
            <span class="hljs-type">Button</span>(<span class="hljs-string">"Ok"</span>, role: .cancel) { }
        }, message: {
            <span class="hljs-type">Text</span>(authService.authErrorMessage)
        })
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">signUp</span><span class="hljs-params">()</span></span> {
        <span class="hljs-type">Task</span> {
            await authService.signUp(username: emailAddress)
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">login</span><span class="hljs-params">()</span></span> {
        <span class="hljs-type">Task</span> {
            await authService.login(username: emailAddress)
        }
    }
}

#<span class="hljs-type">Preview</span> {
    <span class="hljs-type">LoginView</span>()
}
</code></pre>
<p>Here's a quick run down on the screen:</p>
<ul>
<li><p>There is a <code>TextField</code> for username/email input. I'm also using a <code>FocusState</code> so the keyboard will be dismissed when the return button is tapped since the buttons are at the bottom of the screen and will be covered up</p>
</li>
<li><p>I've also got a <code>Text</code> label in the mix that gets updated with a status as we progress through the flow</p>
</li>
<li><p>This screen can also show an error alert if anything goes wrong.</p>
</li>
</ul>
<h4 id="heading-a-quick-note-about-ngrok">A Quick Note about ngrok</h4>
<p><code>ngrok</code> is a tool that creates an http or https endpoint and routes the traffic to your local machine. You're going to need this to make calls to your local server during testing. You can download ngrok for free <a target="_blank" href="https://ngrok.com/downloads/mac-os">here</a>. Once you get it set up, all you have to do is run <code>./ngrok http 8080</code> in Terminal and you'll be able to connect to your local server with the URL it provides. The https version of the URL will be your relying party, and the base URL for connecting to your backend server. Don't forget to update those values if you turn ngrok off and back on again, because the URL will change.</p>
<h4 id="heading-setting-up-an-associated-domain">Setting up an Associated Domain</h4>
<p>Once you have ngrok running, you'll need to use that https url in a couple places in your Xcode project. Open your project file, select your app's target and select the <code>Signing &amp; Capabilties</code> tab. Add an <code>Associated Domains</code> capability, and then add a domain like this:</p>
<p><code>webcredentials:7c42-2600-1700-3e41-88b0-b052-7126-dc42-525f.ngrok-free.app?mode=developer</code></p>
<p>When you've got a real url set up and are ready for production, you'll remove the <code>?mode=developer</code> part, but it's going to allow us to reach our url for testing purposes. Here's a link if you'd like to read more about <a target="_blank" href="https://developer.apple.com/documentation/xcode/supporting-associated-domains">supporting associated domains</a> on iOS.</p>
<p>We're also going to use the ngrok url as our relying party in <code>AuthService</code>, which is coming up next. Also note, each time you kill and restart ngrok, you're going to need to update both places in your iOS code with the new url. You'll also need to update the environment variables on your Vapor server and run it again.</p>
<h2 id="heading-setting-up-authservice">Setting up AuthService</h2>
<p>Before we get into the heavy lifting, here's an enum I created to help keep track of progress as the app moves through the steps of Passkey creation and login. We'll display the status in the <code>Text</code> label on the login screen.</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">AuthStatus</span>: <span class="hljs-title">Equatable</span>, <span class="hljs-title">Hashable</span> </span>{
    <span class="hljs-keyword">case</span> signUpInProgress
    <span class="hljs-keyword">case</span> signUpSuccess
    <span class="hljs-keyword">case</span> loginInProgress
    <span class="hljs-keyword">case</span> loginSuccess
    <span class="hljs-keyword">case</span> loggedOut

    <span class="hljs-keyword">var</span> displayMessage: <span class="hljs-type">String</span> {
        <span class="hljs-keyword">switch</span> <span class="hljs-keyword">self</span> {
        <span class="hljs-keyword">case</span> .signUpInProgress:     <span class="hljs-string">"Sign up in progress..."</span>
        <span class="hljs-keyword">case</span> .signUpSuccess:        <span class="hljs-string">"Signed up successfully"</span>
        <span class="hljs-keyword">case</span> .loginInProgress:      <span class="hljs-string">"Logging in..."</span>
        <span class="hljs-keyword">case</span> .loginSuccess:         <span class="hljs-string">"Logged in!"</span>
        <span class="hljs-keyword">case</span> .loggedOut:            <span class="hljs-string">"Logged out"</span>
        }
    }
}
</code></pre>
<p>We can start with this basic outline of the file:</p>
<pre><code class="lang-swift"><span class="hljs-comment">// 1. Class setup</span>
@<span class="hljs-type">MainActor</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthService</span>: <span class="hljs-title">NSObject</span>, <span class="hljs-title">ObservableObject</span> </span>{

    <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">PasskeyError</span>: <span class="hljs-title">Error</span> </span>{
        <span class="hljs-keyword">case</span> signup
        <span class="hljs-keyword">case</span> login
    }

    <span class="hljs-comment">// 2. Relying party url created by ngrok</span>
    <span class="hljs-keyword">static</span> <span class="hljs-keyword">let</span> relyingParty = <span class="hljs-string">"7c42-2600-1700-3e41-88b0-b052-7126-dc42-525f.ngrok-free.app"</span>

    <span class="hljs-comment">// 3. Published properties for the LoginView</span>
    @<span class="hljs-type">Published</span> <span class="hljs-keyword">var</span> authErrorTitle = <span class="hljs-string">""</span>
    @<span class="hljs-type">Published</span> <span class="hljs-keyword">var</span> authErrorMessage = <span class="hljs-string">""</span>
    @<span class="hljs-type">Published</span> <span class="hljs-keyword">var</span> isShowingAuthError = <span class="hljs-literal">false</span>
    @<span class="hljs-type">Published</span> <span class="hljs-keyword">var</span> authStatus = <span class="hljs-type">AuthStatus</span>.loggedOut

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> username = <span class="hljs-string">""</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> userId = <span class="hljs-string">""</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> preferImmediatelyAvailableCredentials = <span class="hljs-literal">true</span>

    <span class="hljs-comment">// MARK: - Sign Up / Registration</span>

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">signUp</span><span class="hljs-params">(username: String)</span></span> async {
        authStatus = .signUpInProgress
    }

    <span class="hljs-comment">// MARK: - Log in</span>

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">login</span><span class="hljs-params">(username: String)</span></span> async {
        authStatus = .loginInProgress
        <span class="hljs-keyword">self</span>.username = username
    }
}
</code></pre>
<p>This is just a basic outline of a file. I'll give a quick overview and then we'll start implementing everything:</p>
<ol>
<li><p>I'm using <code>ObservableObject</code> and publishing some values to be observed by the UI. If you're targeting iOS 17 and up, you can use the <code>@Observable</code> macro instead. Everything in this article will work with iOS 16+ since that's what I'm supporting in my apps at the time of writing.</p>
</li>
<li><p>Here's our <a target="_blank" href="https://www.w3.org/TR/webauthn-2/#relying-party">relying party</a>, using the domain created for us by ngrok. You'll eventually replace this with your real url in a production app</p>
</li>
<li><p>We're publishing some properties that will drive the UI</p>
</li>
</ol>
<p>You can also see I've got some placeholder functions for the overall purpose of <code>AuthService</code>, which is handling sign ups (Passkey creation), and logging in with an existing passkey.</p>
<h3 id="heading-making-calls-to-the-passkey-server">Making Calls to the Passkey Server</h3>
<p>You can use your preferred method for making API calls, but if you just want to get this all working, here's my code for making GET and POST requests. It's a simplified version of what I've been using for years, and for our purposes now, it'll just work. This code takes a generic type <code>T</code> and decodes the response from the server into that type, or throws an error.</p>
<pre><code class="lang-swift"><span class="hljs-comment">//</span>
<span class="hljs-comment">//  URLSession+Example.swift</span>
<span class="hljs-comment">//  PasskeyClient</span>
<span class="hljs-comment">//</span>

<span class="hljs-keyword">import</span> Foundation

<span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">SessionError</span>: <span class="hljs-title">Error</span> </span>{
    <span class="hljs-keyword">case</span> badResponse
    <span class="hljs-keyword">case</span> requestFailed(<span class="hljs-type">String</span>)
}

<span class="hljs-comment">/// Object to represent empty response bodies from API calls that conform to Decodable.</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">EmptyResponse</span>: <span class="hljs-title">Decodable</span> </span>{ }

<span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">URLSession</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">get</span>&lt;T: Decodable&gt;<span class="hljs-params">(endpoint: String,
                           queryParams: [String: String] = [:],
                           decodingType: T.<span class="hljs-keyword">Type</span>)</span></span> async <span class="hljs-keyword">throws</span> -&gt; <span class="hljs-type">T</span> {
        <span class="hljs-keyword">var</span> request = request(endpoint: endpoint, queryParams: queryParams)
        request.httpMethod = <span class="hljs-string">"GET"</span>
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">try</span> await fetchAndDecode(request: request)
    }

    <span class="hljs-meta">@discardableResult</span>
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">post</span>&lt;T: Decodable&gt;<span class="hljs-params">(endpoint: String,
                            queryParams: [String: String] = [:],
                            postBody: Data,
                            decodingType: T.<span class="hljs-keyword">Type</span>)</span></span> async <span class="hljs-keyword">throws</span> -&gt; <span class="hljs-type">T</span> {
        <span class="hljs-keyword">var</span> request = request(endpoint: endpoint, queryParams: queryParams)
        request.httpMethod = <span class="hljs-string">"POST"</span>
        request.httpBody = postBody
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">try</span> await fetchAndDecode(request: request)
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">request</span><span class="hljs-params">(endpoint: String,
                         queryParams: [String: String])</span></span> -&gt; <span class="hljs-type">URLRequest</span> {
        <span class="hljs-keyword">let</span> baseUrlString = <span class="hljs-string">"https://\(AuthService.relyingParty)"</span>
        <span class="hljs-keyword">let</span> baseUrl = <span class="hljs-type">URL</span>(string: baseUrlString)!
        <span class="hljs-keyword">let</span> fullUrl = <span class="hljs-type">URL</span>(string: endpoint, relativeTo: baseUrl)!
        <span class="hljs-keyword">var</span> request = <span class="hljs-type">URLRequest</span>(url: fullUrl)

        <span class="hljs-keyword">if</span> !queryParams.isEmpty {
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> url = request.url {
                <span class="hljs-keyword">if</span> <span class="hljs-keyword">var</span> components = <span class="hljs-type">URLComponents</span>(url: url, resolvingAgainstBaseURL: <span class="hljs-literal">true</span>) {
                    <span class="hljs-keyword">let</span> queryItems = queryParams.<span class="hljs-built_in">compactMap</span> { <span class="hljs-type">URLQueryItem</span>(name: $<span class="hljs-number">0</span>.key, value: $<span class="hljs-number">0</span>.value) }
                    components.queryItems = queryItems
                    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> newURL = components.url {
                        request = <span class="hljs-type">URLRequest</span>(url: newURL)
                    }
                }
            }
        }

        defaultHeaders.forEach { request.addValue($<span class="hljs-number">0</span>.value, forHTTPHeaderField: $<span class="hljs-number">0</span>.key) }
        <span class="hljs-keyword">return</span> request
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">fetchAndDecode</span>&lt;T: Decodable&gt;<span class="hljs-params">(request: URLRequest)</span></span> async <span class="hljs-keyword">throws</span> -&gt; <span class="hljs-type">T</span> {
        <span class="hljs-keyword">let</span> (data, response) = <span class="hljs-keyword">try</span> await data(<span class="hljs-keyword">for</span>: request)

        <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> response = response <span class="hljs-keyword">as</span>? <span class="hljs-type">HTTPURLResponse</span> <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">throw</span> <span class="hljs-type">SessionError</span>.badResponse
        }

        <span class="hljs-keyword">switch</span> response.statusCode {
        <span class="hljs-keyword">case</span> (<span class="hljs-number">200</span>...<span class="hljs-number">299</span>):
            <span class="hljs-keyword">var</span> data = data
            <span class="hljs-keyword">if</span> data.isEmpty {
                data = <span class="hljs-string">"{}"</span>.data(using: .utf8) ?? <span class="hljs-type">Data</span>()
            }

            <span class="hljs-keyword">return</span> <span class="hljs-keyword">try</span> <span class="hljs-type">JSONDecoder</span>().decode(<span class="hljs-type">T</span>.<span class="hljs-keyword">self</span>, from: data)

        <span class="hljs-keyword">default</span>:
            <span class="hljs-keyword">let</span> errorMessage = <span class="hljs-type">String</span>(data: data, encoding: .utf8) ?? <span class="hljs-string">""</span>
            <span class="hljs-keyword">throw</span> <span class="hljs-type">SessionError</span>.requestFailed(errorMessage)
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> defaultHeaders: [<span class="hljs-type">String</span>: <span class="hljs-type">String</span>] {
        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
            <span class="hljs-string">"Accept"</span>: <span class="hljs-string">"application/json"</span>
        ]
    }
}
</code></pre>
<h3 id="heading-creating-a-passkey">Creating a Passkey</h3>
<p>First we've got to create a Passkey. If you're building on my <a target="_blank" href="https://whiterockstudios.hashnode.dev/creating-a-passkey-server-for-ios-and-android-in-swift">vapor Passkey server article</a>, you can enter any username or email address or whatever into the <code>TextField</code> and tap the <code>Sign Up</code> button. It's going to call this function in AuthService:</p>
<pre><code class="lang-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">signUp</span><span class="hljs-params">(username: String)</span></span> async {
        authStatus = .signUpInProgress

        <span class="hljs-keyword">do</span> {
            <span class="hljs-comment">// 1. Generate a challenge</span>
            <span class="hljs-keyword">let</span> challengeResponse = <span class="hljs-keyword">try</span> await <span class="hljs-type">URLSession</span>.shared.<span class="hljs-keyword">get</span>(endpoint: <span class="hljs-string">"/signup"</span>,
                                                                    queryParams: [<span class="hljs-string">"username"</span>: username],
                                                                    decodingType: <span class="hljs-type">ChallengeResponse</span>.<span class="hljs-keyword">self</span>)

            <span class="hljs-comment">// 2. Save the userId -- we'll need it later                                                                    </span>
            userId = challengeResponse.userId

            <span class="hljs-comment">// 3. Trigger iOS to prompt the user to create a Passkey</span>
            <span class="hljs-keyword">try</span> startSignUpAuthorization(with: challengeResponse)
        } <span class="hljs-keyword">catch</span> {
            <span class="hljs-comment">// 4. Something went wrong, kick the error up to the UI</span>
            authErrorTitle = <span class="hljs-string">"Sign Up Error"</span>
            authErrorMessage = <span class="hljs-string">"\(error)"</span>
            isShowingAuthError = <span class="hljs-literal">true</span>
        }
    }
</code></pre>
<p>Here's what we're adding to the login function:</p>
<ol>
<li><p>We've got to call our first server endpoint with the username supplied by the user. The server will make sure a Passkey doesn't already exist for this user, create a challenge, and save it to the database. That challenge and the user's id will be returned to us. In a production app, you'd already have a user and user id, but this is a simplified flow focusing on the Passkey parts.</p>
</li>
<li><p>Save the <code>userId</code> locally, because we're going to need it again later in the process.</p>
</li>
<li><p>Here we start the flow in iOS that's going to prompt the user to create a Passkey. For most users, they'll get a prompt to allow the Passkey and be presented with a Face ID authentication (see screenshot below).</p>
</li>
<li><p>If anything goes wrong, we're going to bubble the error state back up to the UI so you know what's going on.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742673828427/b1294690-4b83-4cc3-9221-242179291fee.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-the-challengeresponse-struct">The ChallengeResponse struct</h4>
<p>Here's how <code>ChallengeResponse</code> is built. I'm using <code>CustomStringConvertible</code> to generate a nicer log output for debugging.</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ChallengeResponse</span>: <span class="hljs-title">Decodable</span>, <span class="hljs-title">CustomStringConvertible</span> </span>{
    <span class="hljs-keyword">let</span> challenge: <span class="hljs-type">Challenge</span>
    <span class="hljs-keyword">let</span> userId: <span class="hljs-type">String</span>

    <span class="hljs-keyword">var</span> description: <span class="hljs-type">String</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-string">"""
            challenge: \(challenge)
            userId: \(userId)
        """</span>
    }
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Challenge</span>: <span class="hljs-title">Decodable</span>, <span class="hljs-title">CustomStringConvertible</span> </span>{
    <span class="hljs-keyword">let</span> attestation: <span class="hljs-type">String</span>
    <span class="hljs-keyword">let</span> challenge: <span class="hljs-type">String</span>
    <span class="hljs-keyword">let</span> params: [<span class="hljs-type">PublicKeyCredParams</span>]
    <span class="hljs-keyword">let</span> relyingParty: <span class="hljs-type">RelyingParty</span>
    <span class="hljs-keyword">let</span> timeout: <span class="hljs-type">Int</span>
    <span class="hljs-keyword">let</span> user: <span class="hljs-type">AuthUser</span>

    <span class="hljs-keyword">var</span> description: <span class="hljs-type">String</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-string">"""
            attestation: \(attestation)
            challenge: \(challenge)
            params: \(params)
            relyingParty: \(relyingParty)
            timeout: \(timeout)
            user: \(user)
        """</span>
    }

    <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">CodingKeys</span>: <span class="hljs-title">String</span>, <span class="hljs-title">CodingKey</span> </span>{
        <span class="hljs-keyword">case</span> attestation
        <span class="hljs-keyword">case</span> challenge
        <span class="hljs-keyword">case</span> params = <span class="hljs-string">"pubKeyCredParams"</span>
        <span class="hljs-keyword">case</span> relyingParty = <span class="hljs-string">"rp"</span>
        <span class="hljs-keyword">case</span> timeout
        <span class="hljs-keyword">case</span> user
    }
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">PublicKeyCredParams</span>: <span class="hljs-title">Decodable</span>, <span class="hljs-title">CustomStringConvertible</span> </span>{
    <span class="hljs-keyword">let</span> alg: <span class="hljs-type">Int</span>
    <span class="hljs-keyword">let</span> type: <span class="hljs-type">String</span>

    <span class="hljs-keyword">var</span> description: <span class="hljs-type">String</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-string">"alg: \(alg), type: \(type)"</span>
    }
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">RelyingParty</span>: <span class="hljs-title">Decodable</span>, <span class="hljs-title">CustomStringConvertible</span> </span>{
    <span class="hljs-keyword">let</span> id: <span class="hljs-type">String</span>
    <span class="hljs-keyword">let</span> name: <span class="hljs-type">String</span>

    <span class="hljs-keyword">var</span> description: <span class="hljs-type">String</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-string">"id: \(id), name: \(name)"</span>
    }
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">AuthUser</span>: <span class="hljs-title">Decodable</span>, <span class="hljs-title">CustomStringConvertible</span> </span>{
    <span class="hljs-keyword">let</span> displayName: <span class="hljs-type">String</span>
    <span class="hljs-keyword">let</span> id: <span class="hljs-type">String</span>
    <span class="hljs-keyword">let</span> name: <span class="hljs-type">String</span>

    <span class="hljs-keyword">var</span> description: <span class="hljs-type">String</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-string">"id: \(id), name: \(name), displayName: \(displayName)"</span>
    }
}
</code></pre>
<h3 id="heading-generating-a-passkey-on-ios-with-asauthorizationcontroller">Generating a Passkey on iOS with ASAuthorizationController</h3>
<p>/Users/jday/Documents/WhiteRockStudios/blog/passkeys/iOS screenshots/iOS allow Face ID.png</p>
<p>/Users/jday/Documents/WhiteRockStudios/blog/passkeys/iOS screenshots/iOS save Passkey.png</p>
<p>Once you've got <code>ChallengeResponse</code> and its properties set up, we're ready to continue. From step 3 above, we call <code>startSignUpAuthorization</code> and pass in our newly created challenge. We're going to create a request and pass that into <code>ASAuthorizationController</code>. This will bring up the system Passkey screen in the screenshot above. This will prompt the user to allow biometrics and use them to create a Passkey. Most users at this point will see Face ID, but Touch ID would also work. Once that is finished, we'll get a callback from <code>ASAuthorizationControllerDelegate</code>, which <code>AuthService</code> must conform to and implement.</p>
<p>Note: You can enable biometrics in the iOS Simulator with the <code>enroll</code> option in the <code>Features &gt; Face ID</code> menu. You'll see that you can also complete Face ID prompts here (and there's a handy keyboard shortcut).</p>
<p>Let's walk through <code>startSignUpAuthorization</code> first:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">startSignUpAuthorization</span><span class="hljs-params">(with challenge: ChallengeResponse)</span></span> <span class="hljs-keyword">throws</span> {
    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> challengeData = challenge.challenge.challenge.data(using: .utf8),
          <span class="hljs-keyword">let</span> userIdData = challenge.challenge.user.id.data(using: .utf8) <span class="hljs-keyword">else</span> {

        <span class="hljs-keyword">throw</span> <span class="hljs-type">PasskeyError</span>.signup
    }

    <span class="hljs-comment">// 1. Create the request using our relying party and challenge data</span>
    <span class="hljs-keyword">let</span> platformProvider = <span class="hljs-type">ASAuthorizationPlatformPublicKeyCredentialProvider</span>(
            relyingPartyIdentifier: <span class="hljs-type">AuthService</span>.relyingParty)
    <span class="hljs-keyword">let</span> assertionKeyRequest = platformProvider.createCredentialRegistrationRequest(challenge: challengeData,
                                                                                   name: challenge.challenge.user.name,
                                                                                   userID: userIdData)

    <span class="hljs-comment">// 2. Establish AuthController as the delegate and send the request to create a Passkey</span>
    <span class="hljs-keyword">let</span> authController = <span class="hljs-type">ASAuthorizationController</span>(authorizationRequests: [assertionKeyRequest])
    authController.delegate = <span class="hljs-keyword">self</span>
    authController.presentationContextProvider = <span class="hljs-keyword">self</span>
    authController.performRequests()
}
</code></pre>
<p>So what exactly is going on above?</p>
<ol>
<li><p>After guarding to make sure we've got all the challenge data inputs required, we create an assertion request using the <code>AuthenticationServices</code> framework in iOS.</p>
</li>
<li><p>The request gets passed to <code>ASAuthorizationController</code> to prompt the user to allow the Passkey to be created. Our <code>AuthService</code> class will be the controller's delegate to handle the result. We also provide a presentation context. At the end of your file, add the below chunk of code.</p>
</li>
</ol>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">AuthService</span>: <span class="hljs-title">ASAuthorizationControllerPresentationContextProviding</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">presentationAnchor</span><span class="hljs-params">(<span class="hljs-keyword">for</span> controller: ASAuthorizationController)</span></span> -&gt; <span class="hljs-type">ASPresentationAnchor</span> {
        <span class="hljs-type">ASPresentationAnchor</span>()
    }
}
</code></pre>
<p>Ok, back to the <code>ASAuthorizationControllerDelegate</code> conformance. You'll need to implement the two functions below. One is for error handling, and the other handles the actual responses from ASAuthorizationController. The same delegate callback function handles registration, signing in (credential assertion), and also other types of login like <strong>Sign in with Apple</strong>. Here's the first step in getting this implementation ready:</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">AuthService</span>: <span class="hljs-title">ASAuthorizationControllerDelegate</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">authorizationController</span><span class="hljs-params">(controller: ASAuthorizationController, didCompleteWithError error: Error)</span></span> {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"\(#function) error: \(error)"</span>)
    }

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">authorizationController</span><span class="hljs-params">(controller: ASAuthorizationController,
                                 didCompleteWithAuthorization authorization: ASAuthorization)</span></span> {
        <span class="hljs-keyword">switch</span> authorization.credential {
        <span class="hljs-keyword">case</span> <span class="hljs-keyword">let</span> credentialRegistration <span class="hljs-keyword">as</span> <span class="hljs-type">ASAuthorizationPlatformPublicKeyCredentialRegistration</span>:
            <span class="hljs-built_in">print</span>(<span class="hljs-string">"\(#function) got a credential registration: \(credentialRegistration)"</span>)

            <span class="hljs-type">Task</span> {
                <span class="hljs-keyword">do</span> {
                    <span class="hljs-keyword">try</span> await makeCredential(userId: userId,
                                             registration: credentialRegistration)
                } <span class="hljs-keyword">catch</span> {
                    authErrorTitle = <span class="hljs-string">"Error Registering Credentials"</span>
                    authErrorMessage = <span class="hljs-string">"Unable to register at this time. Reason: \(error)"</span>
                    isShowingAuthError = <span class="hljs-literal">true</span>
                }
            }

        <span class="hljs-keyword">case</span> <span class="hljs-keyword">let</span> credentialAssertion <span class="hljs-keyword">as</span> <span class="hljs-type">ASAuthorizationPlatformPublicKeyCredentialAssertion</span>:
            <span class="hljs-comment">// we are not ready for this yet</span>
            <span class="hljs-keyword">break</span>

       <span class="hljs-keyword">case</span> <span class="hljs-keyword">let</span> credential <span class="hljs-keyword">as</span> <span class="hljs-type">ASPasswordCredential</span>:
             <span class="hljs-comment">// Handle other authentication cases, such as Sign in with Apple. We are not doing anything with this.</span>
             <span class="hljs-built_in">print</span>(<span class="hljs-string">"\(#function) got a password credential: \(credential)"</span>)

        <span class="hljs-keyword">default</span>:
            <span class="hljs-built_in">print</span>(<span class="hljs-string">"something went wrong: \(authorization)"</span>)
        }
    }
}
</code></pre>
<p>All we're really doing above is making sure the controller is handling a credential registration request, by making sure the authorization credential is of type <code>ASAuthorizationPlatformPublicKeyCredentialRegistration</code>. If so, we're going to take that credential registration and send it to our server in the <code>makeCredential</code> function. There, we'll create a POST request and send that up to our /makeCredential endpoint. If all goes well there, we've completed the registration process and can now use our Passkey to sign in. Here's what that implementation looks like:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">makeCredential</span><span class="hljs-params">(userId: String,
                                registration: ASAuthorizationPlatformPublicKeyCredentialRegistration)</span></span> async <span class="hljs-keyword">throws</span> {
    <span class="hljs-keyword">self</span>.userId = userId

    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> attestationObject = registration.rawAttestationObject <span class="hljs-keyword">else</span> {
            authErrorTitle = <span class="hljs-string">"Login Error"</span>
            authErrorMessage = <span class="hljs-string">"Unable to find attestation object"</span>
            isShowingAuthError = <span class="hljs-literal">true</span>
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">let</span> payload = attestationPayload(userId: userId,
                                     registration: registration,
                                     attestation: attestationObject)
    <span class="hljs-keyword">try</span> await <span class="hljs-type">URLSession</span>.shared.post(endpoint: <span class="hljs-string">"/makeCredential"</span>,
                                     queryParams: [<span class="hljs-string">"userId"</span>: userId],
                                     postBody: <span class="hljs-type">JSONSerialization</span>.data(withJSONObject: payload),
                                     decodingType: <span class="hljs-type">EmptyResponse</span>.<span class="hljs-keyword">self</span>)

    authStatus = .signUpSuccess
}

<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">attestationPayload</span><span class="hljs-params">(userId: String,
                                registration: ASAuthorizationPlatformPublicKeyCredentialRegistration,
                                attestation: Data)</span></span> -&gt; [<span class="hljs-type">String</span>: <span class="hljs-type">Any</span>] {

    <span class="hljs-keyword">let</span> clientDataJSON = registration.rawClientDataJSON
    <span class="hljs-keyword">let</span> credentialID = registration.credentialID

    <span class="hljs-keyword">return</span> [
        <span class="hljs-string">"rawId"</span>: credentialID.base64EncodedString(),
        <span class="hljs-string">"id"</span>: registration.credentialID.base64URLEncode(),
        <span class="hljs-string">"authenticatorAttachment"</span>: <span class="hljs-string">"platform"</span>, <span class="hljs-comment">// Optional parameter</span>
        <span class="hljs-string">"clientExtensionResults"</span>: [<span class="hljs-type">String</span>: <span class="hljs-type">Any</span>](), <span class="hljs-comment">// Optional parameter</span>
        <span class="hljs-string">"type"</span>: <span class="hljs-string">"public-key"</span>,
        <span class="hljs-string">"response"</span>: [
            <span class="hljs-string">"attestationObject"</span>: attestation.base64EncodedString(),
            <span class="hljs-string">"clientDataJSON"</span>: clientDataJSON.base64EncodedString()
        ],
        userId: userId
    ]
}
</code></pre>
<p>So what's going on here? We're basically just building a POST payload out of the credential response we got back from <code>ASAuthenticationController</code> and sending that to the server. It returns as empty 200 response if everything goes well. The <code>attestationPayload</code> function builds up that payload, making sure all of the fields are encoded correctly. As mentioned before, there's heavy use of base64 and base64 url encoded strings in the WebAuthn flow. If you're interested in learning more about how WebAuthn works, you can really just google any of those terms or ask your favorite LLM, and go as deep into the details as you'd like.</p>
<h4 id="heading-a-quick-note-about-aspresenationanchor-above">A quick note about ASPresenationAnchor above</h4>
<p>There's no need to provide a window or try to hook into your UI somehow. Other implementations you'll find online here are often misunderstood and overcomplicated. By just calling <code>ASPresentationAnchor()</code>, iOS will do the right thing.</p>
<h3 id="heading-signing-in-with-a-passkey">Signing in with a Passkey</h3>
<p>Now we're ready to use the <code>Log In</code> button in our iOS app, and validate our newly created Passkey. Our button calls <code>AuthService.login</code>, which gets implemented like this:</p>
<pre><code class="lang-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">login</span><span class="hljs-params">(username: String)</span></span> async {
    authStatus = .loginInProgress
    <span class="hljs-keyword">self</span>.username = username

    <span class="hljs-keyword">do</span> {
        <span class="hljs-keyword">let</span> options = <span class="hljs-keyword">try</span> await <span class="hljs-type">URLSession</span>.shared.<span class="hljs-keyword">get</span>(endpoint: <span class="hljs-string">"/authenticate"</span>,
                                                      queryParams: [<span class="hljs-string">"username"</span>: username],
                                                      decodingType: <span class="hljs-type">PublicKeyCredentialRequestOptions</span>.<span class="hljs-keyword">self</span>)

        <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> challengeData = options.challenge.data(using: .utf8) {
            initiateLogin(challenge: challengeData)
        } <span class="hljs-keyword">else</span> {
            authErrorTitle = <span class="hljs-string">"Error Logging In"</span>
            authErrorMessage = <span class="hljs-string">"Things are not working."</span>
            isShowingAuthError = <span class="hljs-literal">true</span>
        }
    } <span class="hljs-keyword">catch</span> {
        authErrorTitle = <span class="hljs-string">"Login Error"</span>
        authErrorMessage = <span class="hljs-string">"\(error)"</span>
        isShowingAuthError = <span class="hljs-literal">true</span>
    }
}
</code></pre>
<p>Pretty straightforward here, we're making a GET call to our server's <code>/authenticate</code> endpoint to retreive challenge data for a Passkey login. We're going to pass that challenge to our <code>initiateLogin</code> function which is going to interact with <code>ASAuthorizationController</code> again. Before we get there, here is the model class structure for <code>PublicKeyCredentialRequestOptions</code>:</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">PublicKeyCredentialRequestOptions</span>: <span class="hljs-title">Decodable</span> </span>{
    <span class="hljs-comment">/// A base64encoded string.</span>
    <span class="hljs-keyword">let</span> challenge: <span class="hljs-type">String</span>

    <span class="hljs-keyword">let</span> timeout: <span class="hljs-type">UInt32?</span>
    <span class="hljs-keyword">let</span> rpId: <span class="hljs-type">String?</span>
    <span class="hljs-keyword">let</span> allowCredentials: [<span class="hljs-type">PublicKeyCredentialDescriptor</span>]?
    <span class="hljs-keyword">let</span> userVerification: <span class="hljs-type">UserVerificationRequirement?</span>
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">PublicKeyCredentialDescriptor</span>: <span class="hljs-title">Decodable</span> </span>{
    <span class="hljs-keyword">let</span> id: <span class="hljs-type">String</span>
    <span class="hljs-keyword">let</span> type: <span class="hljs-type">String</span>
    <span class="hljs-keyword">let</span> transports: [<span class="hljs-type">String</span>]
}

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">UserVerificationRequirement</span>: <span class="hljs-title">String</span>, <span class="hljs-title">Decodable</span> </span>{
    <span class="hljs-comment">/// The auth server requires user verification and will fail if the user wasn't verified</span>
    <span class="hljs-keyword">case</span> <span class="hljs-keyword">required</span>
    <span class="hljs-comment">/// The auth servier prefers user verification if possible, but will not fail without it</span>
    <span class="hljs-keyword">case</span> preferred
    <span class="hljs-comment">/// The auth server does not want user verification</span>
    <span class="hljs-keyword">case</span> discouraged
}
</code></pre>
<p>Now for implementation of <code>initiateLogin</code>:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">initiateLogin</span><span class="hljs-params">(challenge: Data)</span></span> {
    <span class="hljs-comment">// 1. Create an assertion request</span>
    <span class="hljs-keyword">let</span> publicKeyCredentialProvider = <span class="hljs-type">ASAuthorizationPlatformPublicKeyCredentialProvider</span>(
            relyingPartyIdentifier: <span class="hljs-type">AuthService</span>.relyingParty)
    <span class="hljs-keyword">let</span> assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest(challenge: challenge)

    <span class="hljs-keyword">let</span> authController = <span class="hljs-type">ASAuthorizationController</span>(authorizationRequests: [assertionRequest])
    authController.delegate = <span class="hljs-keyword">self</span>
    authController.presentationContextProvider = <span class="hljs-keyword">self</span>

    <span class="hljs-comment">// 2. Determine the flow to use with ASAuthorizationController</span>
    <span class="hljs-keyword">if</span> preferImmediatelyAvailableCredentials {
        authController.performRequests(options: <span class="hljs-type">ASAuthorizationController</span>.<span class="hljs-type">RequestOptions</span>.preferImmediatelyAvailableCredentials)
    } <span class="hljs-keyword">else</span> {
        authController.performRequests()
    }
}
</code></pre>
<p>A little more detail on what's happening above:</p>
<ol>
<li><p>We're setting up a public key provider for our relying party, and attaching the challenge from our server to that. Since we're logging in instead of creating a new credential, we create an assertion request and will handle that with <code>ASAuthorizationController</code>, using the delegate we set up earlier during the registration implmentation.</p>
</li>
<li><p>For now, we're just going to prefer immediately available credentials. This means that iOS will put up a modal with our saved Passkeys and use biometrics to authenticate the user. If that goes well, we'll get a good callback in the delegate and can continue the process. In the alternative flow, iOS will present a modal sheet with a QR code to use a Passkey from some other device. We are not going to cover that in this article.</p>
</li>
</ol>
<p>Here's what that modal looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742674079167/0ddd2ce0-c895-4391-9721-e9913df2e85c.png" alt class="image--center mx-auto" /></p>
<p>Back to our <code>ASAuthorizationControllerDelegate</code> implementation:</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">AuthService</span>: <span class="hljs-title">ASAuthorizationControllerDelegate</span> </span>{
    ...

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">authorizationController</span><span class="hljs-params">(controller: ASAuthorizationController,
                                 didCompleteWithAuthorization authorization: ASAuthorization)</span></span> {
        <span class="hljs-keyword">switch</span> authorization.credential {
            ...

            <span class="hljs-keyword">case</span> <span class="hljs-keyword">let</span> credentialAssertion <span class="hljs-keyword">as</span> <span class="hljs-type">ASAuthorizationPlatformPublicKeyCredentialAssertion</span>:
             <span class="hljs-built_in">print</span>(<span class="hljs-string">"\(#function) got a credential assertion (passkey used to sign in): \(credentialAssertion)"</span>)

            <span class="hljs-keyword">if</span> !username.isEmpty {
                <span class="hljs-type">Task</span> {
                    <span class="hljs-keyword">do</span> {
                        <span class="hljs-keyword">try</span> await authenticate(username: username,
                                               assertion: credentialAssertion)
                    } <span class="hljs-keyword">catch</span> {
                        authErrorTitle = <span class="hljs-string">"Error Logging In"</span>
                        authErrorMessage = <span class="hljs-string">"Unable to log in. Reason: \(error)"</span>
                        isShowingAuthError = <span class="hljs-literal">true</span>
                    }
                }
            } <span class="hljs-keyword">else</span> {
                authErrorTitle = <span class="hljs-string">"Missing Data"</span>
                authErrorMessage = <span class="hljs-string">"Username is required."</span>
                isShowingAuthError = <span class="hljs-literal">true</span>
            }

            ...
        }
    }
}
</code></pre>
<p>Now we're handling the case where the authorization credential is a <code>ASAuthorizationPlatformPublicKeyCredentialAssertion</code>. If that's what we got back from ASAuthorizationController, we're going to pass that to our <code>authenticate</code> function, which will verify the credential on our server:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">authenticate</span><span class="hljs-params">(username: String,
                          assertion: ASAuthorizationPlatformPublicKeyCredentialAssertion)</span></span> async <span class="hljs-keyword">throws</span> {        
    <span class="hljs-keyword">do</span> {
        <span class="hljs-keyword">let</span> payload = credentialPayload(credentialAssertion: assertion)
        <span class="hljs-keyword">try</span> await <span class="hljs-type">URLSession</span>.shared.post(endpoint: <span class="hljs-string">"/authenticate"</span>,
                                         queryParams: [<span class="hljs-string">"username"</span>: username],
                                         postBody: <span class="hljs-type">JSONSerialization</span>.data(withJSONObject: payload),
                                         decodingType: <span class="hljs-type">EmptyResponse</span>.<span class="hljs-keyword">self</span>)

        authStatus = .loginSuccess
    } <span class="hljs-keyword">catch</span> {
        authErrorTitle = <span class="hljs-string">"Login Error"</span>
        authErrorMessage = <span class="hljs-string">"\(error)"</span>
        isShowingAuthError = <span class="hljs-literal">true</span>
    }
}
</code></pre>
<p>This calls our <code>/authenticate</code> endpoint, and returns a 200 success response if authentication of the Passkey succeeds. In a production application, you'd want to return some kind of token here that the app will save for subsequent API requests. Here's how we build the <code>credentialPayload</code> above:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">credentialPayload</span><span class="hljs-params">(credentialAssertion: ASAuthorizationPlatformPublicKeyCredentialAssertion)</span></span> -&gt; [<span class="hljs-type">String</span>: <span class="hljs-type">Any</span>] {
    <span class="hljs-keyword">let</span> clientDataJSON = credentialAssertion.rawClientDataJSON
    <span class="hljs-keyword">let</span> credentialId = credentialAssertion.credentialID

    <span class="hljs-keyword">return</span> [
        <span class="hljs-string">"rawId"</span>: credentialId.base64EncodedString(),
        <span class="hljs-string">"id"</span>: credentialId.base64URLEncode(),
        <span class="hljs-string">"authenticatorAttachment"</span>: <span class="hljs-string">"platform"</span>, <span class="hljs-comment">// Optional</span>
        <span class="hljs-string">"clientExtensionResults"</span>: [<span class="hljs-type">String</span>: <span class="hljs-type">Any</span>](), <span class="hljs-comment">// Optional</span>
        <span class="hljs-string">"type"</span>: <span class="hljs-string">"public-key"</span>,
        <span class="hljs-string">"response"</span>: [
            <span class="hljs-string">"clientDataJSON"</span>: clientDataJSON.base64EncodedString(),
            <span class="hljs-string">"authenticatorData"</span>: credentialAssertion.rawAuthenticatorData.base64EncodedString(),
            <span class="hljs-string">"signature"</span>: credentialAssertion.signature.base64EncodedString(),
            <span class="hljs-string">"userHandle"</span>: credentialAssertion.userID.base64URLEncode()
        ]
    ]
}
</code></pre>
<p>This kind of payload should be looking pretty familiar by now. We're doing lots of base64 encoding and base 64 url encoding. When I was getting all of this working myself for the first time, I spent the most time getting all of the fields encoded with the right formats. Hopefully having an example laid out like this will save you a lot of time in building your own implementation!</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>If you've made it this far, congrats! You're ready to fire up ngrok in a terminal, use the generated url as your relying party on your vapor server and iOS application, and test it out!</p>
<p>If you got here first, here are the other 2 articles in this series:</p>
<ul>
<li><p><a target="_blank" href="https://whiterockstudios.hashnode.dev/creating-a-passkey-server-for-ios-and-android-in-swift">Build a WebAuthn Passkey server in Swift with Vapor</a></p>
</li>
<li><p><a target="_blank" href="https://whiterockstudios.hashnode.dev/creating-a-passkey-client-for-android">Build an Android client application that uses Passkeys</a></p>
</li>
</ul>
<p>Here's the complete AuthService file for reference:</p>
<pre><code class="lang-swift">
<span class="hljs-keyword">import</span> SwiftUI
<span class="hljs-keyword">import</span> AuthenticationServices
<span class="hljs-keyword">import</span> CoreAPI

<span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">AuthStatus</span>: <span class="hljs-title">Equatable</span>, <span class="hljs-title">Hashable</span> </span>{
    <span class="hljs-keyword">case</span> signUpInProgress
    <span class="hljs-keyword">case</span> signUpSuccess
    <span class="hljs-keyword">case</span> loginInProgress
    <span class="hljs-keyword">case</span> loginSuccess
    <span class="hljs-keyword">case</span> loggedOut

    <span class="hljs-keyword">var</span> displayMessage: <span class="hljs-type">String</span> {
        <span class="hljs-keyword">switch</span> <span class="hljs-keyword">self</span> {
        <span class="hljs-keyword">case</span> .signUpInProgress:     <span class="hljs-string">"Sign up in progress..."</span>
        <span class="hljs-keyword">case</span> .signUpSuccess:        <span class="hljs-string">"Signed up successfully"</span>
        <span class="hljs-keyword">case</span> .loginInProgress:      <span class="hljs-string">"Logging in..."</span>
        <span class="hljs-keyword">case</span> .loginSuccess:         <span class="hljs-string">"Logged in!"</span>
        <span class="hljs-keyword">case</span> .loggedOut:            <span class="hljs-string">"Logged out"</span>
        }
    }
}

@<span class="hljs-type">MainActor</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthService</span>: <span class="hljs-title">NSObject</span>, <span class="hljs-title">ObservableObject</span> </span>{

    <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">PasskeyError</span>: <span class="hljs-title">Error</span> </span>{
        <span class="hljs-keyword">case</span> signup
        <span class="hljs-keyword">case</span> login
    }

    <span class="hljs-keyword">static</span> <span class="hljs-keyword">let</span> relyingParty = <span class="hljs-string">"7c42-2600-1700-3e41-88b0-b052-7126-dc42-525f.ngrok-free.app"</span>

    @<span class="hljs-type">Published</span> <span class="hljs-keyword">var</span> authErrorTitle = <span class="hljs-string">""</span>
    @<span class="hljs-type">Published</span> <span class="hljs-keyword">var</span> authErrorMessage = <span class="hljs-string">""</span>
    @<span class="hljs-type">Published</span> <span class="hljs-keyword">var</span> isShowingAuthError = <span class="hljs-literal">false</span>
    @<span class="hljs-type">Published</span> <span class="hljs-keyword">var</span> authStatus = <span class="hljs-type">AuthStatus</span>.loggedOut

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> username = <span class="hljs-string">""</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> userId = <span class="hljs-string">""</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> preferImmediatelyAvailableCredentials = <span class="hljs-literal">true</span>


    <span class="hljs-comment">// MARK: - Sign Up / Registration</span>

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">signUp</span><span class="hljs-params">(username: String)</span></span> async {
        authStatus = .signUpInProgress

        <span class="hljs-keyword">do</span> {
            <span class="hljs-keyword">let</span> challengeResponse = <span class="hljs-keyword">try</span> await <span class="hljs-type">URLSession</span>.shared.<span class="hljs-keyword">get</span>(endpoint: <span class="hljs-string">"/signup"</span>,
                                                                    queryParams: [<span class="hljs-string">"username"</span>: username],
                                                                    decodingType: <span class="hljs-type">ChallengeResponse</span>.<span class="hljs-keyword">self</span>)
            userId = challengeResponse.userId
            <span class="hljs-keyword">try</span> startSignUpAuthorization(with: challengeResponse)
        } <span class="hljs-keyword">catch</span> {
            authErrorTitle = <span class="hljs-string">"Sign Up Error"</span>
            authErrorMessage = <span class="hljs-string">"\(error)"</span>
            isShowingAuthError = <span class="hljs-literal">true</span>
        }
    }

    <span class="hljs-comment">/// Sends a POST to /makeCredential</span>
    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">makeCredential</span><span class="hljs-params">(userId: String,
                                registration: ASAuthorizationPlatformPublicKeyCredentialRegistration)</span></span> async <span class="hljs-keyword">throws</span> {
        <span class="hljs-keyword">self</span>.userId = userId

        <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> attestationObject = registration.rawAttestationObject <span class="hljs-keyword">else</span> {
            authErrorTitle = <span class="hljs-string">"Login Error"</span>
            authErrorMessage = <span class="hljs-string">"Unable to find attestation object"</span>
            isShowingAuthError = <span class="hljs-literal">true</span>
            <span class="hljs-keyword">return</span>
        }

        <span class="hljs-keyword">let</span> payload = attestationPayload(userId: userId,
                                         registration: registration,
                                         attestation: attestationObject)
        <span class="hljs-keyword">try</span> await <span class="hljs-type">URLSession</span>.shared.post(endpoint: <span class="hljs-string">"/makeCredential"</span>,
                                         queryParams: [<span class="hljs-string">"userId"</span>: userId],
                                         postBody: <span class="hljs-type">JSONSerialization</span>.data(withJSONObject: payload),
                                         decodingType: <span class="hljs-type">EmptyResponse</span>.<span class="hljs-keyword">self</span>)

        authStatus = .signUpSuccess
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">attestationPayload</span><span class="hljs-params">(userId: String,
                                    registration: ASAuthorizationPlatformPublicKeyCredentialRegistration,
                                    attestation: Data)</span></span> -&gt; [<span class="hljs-type">String</span>: <span class="hljs-type">Any</span>] {

        <span class="hljs-keyword">let</span> clientDataJSON = registration.rawClientDataJSON
        <span class="hljs-keyword">let</span> credentialID = registration.credentialID

        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">"rawId"</span>: credentialID.base64EncodedString(),
            <span class="hljs-string">"id"</span>: registration.credentialID.base64URLEncode(),
            <span class="hljs-string">"authenticatorAttachment"</span>: <span class="hljs-string">"platform"</span>, <span class="hljs-comment">// Optional</span>
            <span class="hljs-string">"clientExtensionResults"</span>: [<span class="hljs-type">String</span>: <span class="hljs-type">Any</span>](), <span class="hljs-comment">// Optional</span>
            <span class="hljs-string">"type"</span>: <span class="hljs-string">"public-key"</span>,
            <span class="hljs-string">"response"</span>: [
                <span class="hljs-string">"attestationObject"</span>: attestation.base64EncodedString(),
                <span class="hljs-string">"clientDataJSON"</span>: clientDataJSON.base64EncodedString()
            ],
            userId: userId
        ]
    }

    <span class="hljs-comment">/// Uses the `ChallengeResponse` obtained from the /signup endpoint to start the PassKey flow on iOS</span>
    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">startSignUpAuthorization</span><span class="hljs-params">(with challenge: ChallengeResponse)</span></span> <span class="hljs-keyword">throws</span> {
        <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> challengeData = challenge.challenge.challenge.data(using: .utf8),
              <span class="hljs-keyword">let</span> userIdData = challenge.challenge.user.id.data(using: .utf8) <span class="hljs-keyword">else</span> {

            <span class="hljs-keyword">throw</span> <span class="hljs-type">PasskeyError</span>.signup
        }

        <span class="hljs-keyword">let</span> platformProvider = <span class="hljs-type">ASAuthorizationPlatformPublicKeyCredentialProvider</span>(
            relyingPartyIdentifier: <span class="hljs-type">AuthService</span>.relyingParty)
        <span class="hljs-keyword">let</span> assertionKeyRequest = platformProvider.createCredentialRegistrationRequest(challenge: challengeData,
                                                                                       name: challenge.challenge.user.name,
                                                                                       userID: userIdData)
        <span class="hljs-keyword">let</span> authController = <span class="hljs-type">ASAuthorizationController</span>(authorizationRequests: [assertionKeyRequest])
        authController.delegate = <span class="hljs-keyword">self</span>
        authController.presentationContextProvider = <span class="hljs-keyword">self</span>
        authController.performRequests()
    }



    <span class="hljs-comment">// MARK: - Log in</span>

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">login</span><span class="hljs-params">(username: String)</span></span> async {
        authStatus = .loginInProgress
        <span class="hljs-keyword">self</span>.username = username

        <span class="hljs-keyword">do</span> {
            <span class="hljs-keyword">let</span> options = <span class="hljs-keyword">try</span> await <span class="hljs-type">URLSession</span>.shared.<span class="hljs-keyword">get</span>(endpoint: <span class="hljs-string">"/authenticate"</span>,
                                                          queryParams: [<span class="hljs-string">"username"</span>: username],
                                                          decodingType: <span class="hljs-type">PublicKeyCredentialRequestOptions</span>.<span class="hljs-keyword">self</span>)

            <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> challengeData = options.challenge.data(using: .utf8) {
                initiateLogin(challenge: challengeData)
            } <span class="hljs-keyword">else</span> {
                authErrorTitle = <span class="hljs-string">"Error Logging In"</span>
                authErrorMessage = <span class="hljs-string">"Things are not working."</span>
                isShowingAuthError = <span class="hljs-literal">true</span>
            }
        } <span class="hljs-keyword">catch</span> {
            authErrorTitle = <span class="hljs-string">"Login Error"</span>
            authErrorMessage = <span class="hljs-string">"\(error)"</span>
            isShowingAuthError = <span class="hljs-literal">true</span>
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">initiateLogin</span><span class="hljs-params">(challenge: Data)</span></span> {
        <span class="hljs-keyword">let</span> publicKeyCredentialProvider = <span class="hljs-type">ASAuthorizationPlatformPublicKeyCredentialProvider</span>(
            relyingPartyIdentifier: <span class="hljs-type">AuthService</span>.relyingParty)
        <span class="hljs-keyword">let</span> assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest(challenge: challenge)

        <span class="hljs-comment">// Pass in any mix of supported sign-in request types.</span>
        <span class="hljs-keyword">let</span> authController = <span class="hljs-type">ASAuthorizationController</span>(authorizationRequests: [assertionRequest])
        authController.delegate = <span class="hljs-keyword">self</span>
        authController.presentationContextProvider = <span class="hljs-keyword">self</span>

        <span class="hljs-keyword">if</span> preferImmediatelyAvailableCredentials {
            authController.performRequests(options: <span class="hljs-type">ASAuthorizationController</span>.<span class="hljs-type">RequestOptions</span>.preferImmediatelyAvailableCredentials)
        } <span class="hljs-keyword">else</span> {
            authController.performRequests()
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">authenticate</span><span class="hljs-params">(username: String,
                              assertion: ASAuthorizationPlatformPublicKeyCredentialAssertion)</span></span> async <span class="hljs-keyword">throws</span> {

        <span class="hljs-keyword">do</span> {
            <span class="hljs-keyword">let</span> payload = credentialPayload(credentialAssertion: assertion)
            <span class="hljs-keyword">try</span> await <span class="hljs-type">URLSession</span>.shared.post(endpoint: <span class="hljs-string">"/authenticate"</span>,
                                             queryParams: [<span class="hljs-string">"username"</span>: username],
                                             postBody: <span class="hljs-type">JSONSerialization</span>.data(withJSONObject: payload),
                                             decodingType: <span class="hljs-type">EmptyResponse</span>.<span class="hljs-keyword">self</span>)

            authStatus = .loginSuccess
        } <span class="hljs-keyword">catch</span> {
            authErrorTitle = <span class="hljs-string">"Login Error"</span>
            authErrorMessage = <span class="hljs-string">"\(error)"</span>
            isShowingAuthError = <span class="hljs-literal">true</span>
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">credentialPayload</span><span class="hljs-params">(credentialAssertion: ASAuthorizationPlatformPublicKeyCredentialAssertion)</span></span> -&gt; [<span class="hljs-type">String</span>: <span class="hljs-type">Any</span>] {
        <span class="hljs-keyword">let</span> clientDataJSON = credentialAssertion.rawClientDataJSON
        <span class="hljs-keyword">let</span> credentialId = credentialAssertion.credentialID

        <span class="hljs-keyword">return</span> [
            <span class="hljs-string">"rawId"</span>: credentialId.base64EncodedString(),
            <span class="hljs-string">"id"</span>: credentialId.base64URLEncode(),
            <span class="hljs-string">"authenticatorAttachment"</span>: <span class="hljs-string">"platform"</span>, <span class="hljs-comment">// Optional</span>
            <span class="hljs-string">"clientExtensionResults"</span>: [<span class="hljs-type">String</span>: <span class="hljs-type">Any</span>](), <span class="hljs-comment">// Optional</span>
            <span class="hljs-string">"type"</span>: <span class="hljs-string">"public-key"</span>,
            <span class="hljs-string">"response"</span>: [
                <span class="hljs-string">"clientDataJSON"</span>: clientDataJSON.base64EncodedString(),
                <span class="hljs-string">"authenticatorData"</span>: credentialAssertion.rawAuthenticatorData.base64EncodedString(),
                <span class="hljs-string">"signature"</span>: credentialAssertion.signature.base64EncodedString(),
                <span class="hljs-string">"userHandle"</span>: credentialAssertion.userID.base64URLEncode()
            ]
        ]
    }
}

<span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">AuthService</span>: <span class="hljs-title">ASAuthorizationControllerDelegate</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">authorizationController</span><span class="hljs-params">(controller: ASAuthorizationController, didCompleteWithError error: Error)</span></span> {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"\(#function) error: \(error)"</span>)
    }

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">authorizationController</span><span class="hljs-params">(controller: ASAuthorizationController,
                                 didCompleteWithAuthorization authorization: ASAuthorization)</span></span> {
        <span class="hljs-keyword">switch</span> authorization.credential {
        <span class="hljs-keyword">case</span> <span class="hljs-keyword">let</span> credentialRegistration <span class="hljs-keyword">as</span> <span class="hljs-type">ASAuthorizationPlatformPublicKeyCredentialRegistration</span>:
            <span class="hljs-built_in">print</span>(<span class="hljs-string">"\(#function) got a credential registration: \(credentialRegistration)"</span>)

            <span class="hljs-type">Task</span> {
                <span class="hljs-keyword">do</span> {
                    <span class="hljs-keyword">try</span> await makeCredential(userId: userId,
                                             registration: credentialRegistration)
                } <span class="hljs-keyword">catch</span> {
                    authErrorTitle = <span class="hljs-string">"Error Registering Credentials"</span>
                    authErrorMessage = <span class="hljs-string">"Unable to register at this time. Reason: \(error)"</span>
                    isShowingAuthError = <span class="hljs-literal">true</span>
                }
            }

        <span class="hljs-keyword">case</span> <span class="hljs-keyword">let</span> credentialAssertion <span class="hljs-keyword">as</span> <span class="hljs-type">ASAuthorizationPlatformPublicKeyCredentialAssertion</span>:
             <span class="hljs-built_in">print</span>(<span class="hljs-string">"\(#function) got a credential assertion (passkey used to sign in): \(credentialAssertion)"</span>)

            <span class="hljs-keyword">if</span> !username.isEmpty {
                <span class="hljs-type">Task</span> {
                    <span class="hljs-keyword">do</span> {
                        <span class="hljs-keyword">try</span> await authenticate(username: username,
                                               assertion: credentialAssertion)
                        <span class="hljs-built_in">print</span>(<span class="hljs-string">"success!"</span>)
                    } <span class="hljs-keyword">catch</span> {
                        authErrorTitle = <span class="hljs-string">"Error Logging In"</span>
                        authErrorMessage = <span class="hljs-string">"Unable to log in. Reason: \(error)"</span>
                        isShowingAuthError = <span class="hljs-literal">true</span>
                    }
                }
            } <span class="hljs-keyword">else</span> {
                authErrorTitle = <span class="hljs-string">"Missing Data"</span>
                authErrorMessage = <span class="hljs-string">"Username is required."</span>
                isShowingAuthError = <span class="hljs-literal">true</span>
            }

        <span class="hljs-keyword">case</span> <span class="hljs-keyword">let</span> credential <span class="hljs-keyword">as</span> <span class="hljs-type">ASPasswordCredential</span>:
             <span class="hljs-comment">// Handle other authentication cases, such as Sign in with Apple.</span>
             <span class="hljs-built_in">print</span>(<span class="hljs-string">"\(#function) got a password credential: \(credential)"</span>)

        <span class="hljs-keyword">default</span>:
            <span class="hljs-built_in">print</span>(<span class="hljs-string">"something went very wrong -- fatal error"</span>)
        }
    }
}

<span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">AuthService</span>: <span class="hljs-title">ASAuthorizationControllerPresentationContextProviding</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">presentationAnchor</span><span class="hljs-params">(<span class="hljs-keyword">for</span> controller: ASAuthorizationController)</span></span> -&gt; <span class="hljs-type">ASPresentationAnchor</span> {
        <span class="hljs-type">ASPresentationAnchor</span>()
    }
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Creating a Passkey Server for iOS and Android in Swift]]></title><description><![CDATA[Passwords are broken. They’re insecure, frustrating for users, and a burden for developers to manage. Passkeys, built on WebAuthn and backed by Apple, Google, and Microsoft, are the future of authentication. They are seamless, secure, and resistant t...]]></description><link>https://blog.whiterockstudios.com/creating-a-passkey-server-for-ios-and-android-in-swift</link><guid isPermaLink="true">https://blog.whiterockstudios.com/creating-a-passkey-server-for-ios-and-android-in-swift</guid><category><![CDATA[passkeys]]></category><category><![CDATA[passkey ]]></category><category><![CDATA[#webauthn]]></category><category><![CDATA[vapor]]></category><category><![CDATA[Swift]]></category><category><![CDATA[iOS]]></category><category><![CDATA[Android]]></category><category><![CDATA[passwordless authentication ]]></category><category><![CDATA[Passwordless]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[mobile]]></category><category><![CDATA[Mobile apps]]></category><category><![CDATA[authentication]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Jeff Day]]></dc:creator><pubDate>Sun, 09 Mar 2025 20:11:48 GMT</pubDate><content:encoded><![CDATA[<p>Passwords are broken. They’re insecure, frustrating for users, and a burden for developers to manage. Passkeys, built on WebAuthn and backed by Apple, Google, and Microsoft, are the future of authentication. They are seamless, secure, and resistant to phishing attacks.</p>
<p>In this series, you’ll learn how to implement passkey support across iOS, Android, and your server backend using Swift, Kotlin, and Vapor. More details about the Swift Webauthn library can be found <a target="_blank" href="https://github.com/swift-server/swift-webauthn">here</a>.</p>
<p>Whether you're building a new app or modernizing an existing one, this guide will help you bring passwordless login to life with a clean approach on both mobile platforms.</p>
<h2 id="heading-overview">Overview</h2>
<p>This project will create a restful server to handle Passkey authentication for iOS and Android apps. MySQL is used for the database layer, although any database supported by Vapor can be used (Postgres, MongoDB, etc... see a full list <a target="_blank" href="https://legacy.docs.vapor.codes/2.0/fluent/database/">here</a>). I’m also interacting with MySQL from the Vapor application with raw SQL instead of using Vapor’s Fluent ORM. More information on Fluent can be found here: <a target="_blank" href="https://legacy.docs.vapor.codes/2.0/fluent/package/">Vapor Fluent</a>.</p>
<h2 id="heading-setting-up-a-vapor-project">Setting up a Vapor Project</h2>
<p>Creating a new Vapor project is easy. You should be able to follow <a target="_blank" href="https://docs.vapor.codes/getting-started/hello-world/">this guide</a> to create a project and open it in Xcode. Once you have your project open, here’s the <code>Package.swift</code> file I’m currently using:</p>
<pre><code class="lang-swift"><span class="hljs-comment">// swift-tools-version:5.9</span>
<span class="hljs-keyword">import</span> PackageDescription

<span class="hljs-keyword">let</span> package = <span class="hljs-type">Package</span>(
    name: “passkey-server”,
    platforms: [
       .macOS(.v13)
    ],
    dependencies: [
        <span class="hljs-comment">// 💧 A server-side Swift web framework.</span>
        .package(url: “https:<span class="hljs-comment">//github.com/vapor/vapor.git”, from: “4.83.1”),</span>
        .package(url: “https:<span class="hljs-comment">//github.com/swift-server/webauthn-swift.git”, from: “0.0.3”),</span>
        .package(url: “https:<span class="hljs-comment">//github.com/vapor/mysql-kit.git”, from: “4.7.0”),</span>
        .package(url: “https:<span class="hljs-comment">//github.com/vapor/fluent.git”, from: “4.8.0”),</span>
        .package(url: “https:<span class="hljs-comment">//github.com/vapor/fluent-mysql-driver.git”, from: “4.4.0”),</span>
    ],
    targets: [
        .executableTarget(
            name: “<span class="hljs-type">App</span>”,
            dependencies: [
                .product(name: “<span class="hljs-type">Vapor</span>”, package: “vapor”),
                .product(name: “<span class="hljs-type">WebAuthn</span>”, package: “webauthn-swift”),
                .product(name: “<span class="hljs-type">MySQLKit</span>”, package: “mysql-kit”),
                .product(name: “<span class="hljs-type">Fluent</span>”, package: “fluent”),
                .product(name: “<span class="hljs-type">FluentMySQLDriver</span>”, package: “fluent-mysql-driver”),
            ]
        ),
        .testTarget(name: “<span class="hljs-type">AppTests</span>”, dependencies: [
            .target(name: “<span class="hljs-type">App</span>”),
            .product(name: “<span class="hljs-type">XCTVapor</span>”, package: “vapor”),

            <span class="hljs-comment">// Workaround for https://github.com/apple/swift-package-manager/issues/6940</span>
            .product(name: “<span class="hljs-type">Vapor</span>”, package: “vapor”),
        ])
    ]
)
</code></pre>
<h3 id="heading-setting-up-the-database">Setting up the Database</h3>
<p>Here are the create table queries to build the MySQL schema for this project:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">user</span> (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">varchar</span>(<span class="hljs-number">36</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  username <span class="hljs-built_in">varchar</span>(<span class="hljs-number">100</span>) <span class="hljs-keyword">DEFAULT</span> <span class="hljs-literal">NULL</span>,
  creation_date datetime <span class="hljs-keyword">DEFAULT</span> <span class="hljs-literal">NULL</span>,
  PRIMARY <span class="hljs-keyword">KEY</span> (<span class="hljs-keyword">id</span>)
) <span class="hljs-keyword">ENGINE</span>=<span class="hljs-keyword">InnoDB</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">CHARSET</span>=utf8mb4 <span class="hljs-keyword">COLLATE</span>=utf8mb4_unicode_ci;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> userauthchallenge (
  user_id <span class="hljs-built_in">varchar</span>(<span class="hljs-number">36</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  challenge <span class="hljs-built_in">text</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  <span class="hljs-keyword">FOREIGN</span> <span class="hljs-keyword">KEY</span> (user_id) 
    <span class="hljs-keyword">REFERENCES</span> <span class="hljs-keyword">user</span>(<span class="hljs-keyword">id</span>)
) <span class="hljs-keyword">ENGINE</span>=<span class="hljs-keyword">InnoDB</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">CHARSET</span>=utf8mb4 <span class="hljs-keyword">COLLATE</span>=utf8mb4_unicode_ci;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> userchallenge (
  user_id <span class="hljs-built_in">varchar</span>(<span class="hljs-number">36</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  challenge <span class="hljs-built_in">text</span>,
  <span class="hljs-keyword">FOREIGN</span> <span class="hljs-keyword">KEY</span> (user_id) 
    <span class="hljs-keyword">REFERENCES</span> <span class="hljs-keyword">user</span>(<span class="hljs-keyword">id</span>)
) <span class="hljs-keyword">ENGINE</span>=<span class="hljs-keyword">InnoDB</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">CHARSET</span>=utf8mb4 <span class="hljs-keyword">COLLATE</span>=utf8mb4_unicode_ci;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> usercredential (
  user_id <span class="hljs-built_in">varchar</span>(<span class="hljs-number">36</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  credential_id <span class="hljs-built_in">varchar</span>(<span class="hljs-number">100</span>),
  public_key <span class="hljs-built_in">text</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  <span class="hljs-keyword">FOREIGN</span> <span class="hljs-keyword">KEY</span> (user_id) 
    <span class="hljs-keyword">REFERENCES</span> <span class="hljs-keyword">user</span>(<span class="hljs-keyword">id</span>)
) <span class="hljs-keyword">ENGINE</span>=<span class="hljs-keyword">InnoDB</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">CHARSET</span>=utf8mb4 <span class="hljs-keyword">COLLATE</span>=utf8mb4_unicode_ci;
</code></pre>
<p>Once these tables are created, you’ll need to set up the databse connection in your Vapor project. In <code>configure.swift</code>, add this line to your <code>configure(_ app: Application)</code> function:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">configure</span><span class="hljs-params">(<span class="hljs-number">_</span> app: Application)</span></span> async <span class="hljs-keyword">throws</span> {
    …
    <span class="hljs-keyword">try</span> setupDatabaseConnection(app)
    …
}

<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">setupDatabaseConnection</span><span class="hljs-params">(<span class="hljs-number">_</span> app: Application)</span></span> <span class="hljs-keyword">throws</span> {
    <span class="hljs-keyword">var</span> tls = <span class="hljs-type">TLSConfiguration</span>.makeClientConfiguration()
    tls.trustRoots = .<span class="hljs-keyword">default</span>
    tls.minimumTLSVersion = .tlsv11
    tls.certificateVerification = .<span class="hljs-keyword">none</span>

<span class="hljs-comment">//    let fileUrl = URL(fileURLWithPath: app.directory.workingDirectory)</span>
<span class="hljs-comment">//        .appendingPathComponent(“Resources/“ + Environment.dbCertPath, isDirectory: false)</span>
<span class="hljs-comment">//</span>
<span class="hljs-comment">//    tls.trustRoots = NIOSSLTrustRoots.certificates(</span>
<span class="hljs-comment">//        try NIOSSLCertificate.fromPEMFile(fileUrl.path)</span>
<span class="hljs-comment">//    )</span>

    <span class="hljs-keyword">let</span> config = <span class="hljs-type">MySQLConfiguration</span>(hostname: <span class="hljs-type">Environment</span>.dbHostName,
                                    username: <span class="hljs-type">Environment</span>.dbUsername,
                                    password: <span class="hljs-type">Environment</span>.dbPassword,
                                    database: <span class="hljs-type">Environment</span>.defaultDB,
                                    tlsConfiguration: tls)
    app.databases.use(.mysql(configuration: config), <span class="hljs-keyword">as</span>: .mysql)
}
</code></pre>
<p>For the purposes of this demo I’m setting up a local MySQL database and ignoring TLS settings. I left the actual settings commented out, so hopefully that can serve as a helpful way to get this up and going if needed. I can't remember where exactly I found this information, but it may have come from the <a target="_blank" href="https://www.google.com/url?sa=t&amp;source=web&amp;rct=j&amp;opi=89978449&amp;url=https://discord.com/invite/vapor&amp;ved=2ahUKEwin9ZD26vWJAxVBG9AFHboFBIMQFnoECA8QAQ&amp;usg=AOvVaw2kE38R55ispczR3T5GZ2gY">vapor discord</a>, it is very active and helpful.</p>
<p>I’m also using environment variables, which require a <code>.env</code> file in the root directory of your project. An example env entry looks pretty standard, like this: <code>DATABASE_URL=localhost</code>.</p>
<p>There’s one more bit of setup I use when I’m working on a vapor project using a database. Even though I’m not using the Fluent ORM, Fluent does help with the management of database connections and exposes a db property on the <code>Request</code> object. I add a little helper extension for even easier access. This will be used later in the part of the app that handles routes:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">import</span> Vapor
<span class="hljs-keyword">import</span> SQLKit
<span class="hljs-keyword">import</span> Fluent

<span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">Request</span> </span>{
    <span class="hljs-keyword">var</span> mySQL: <span class="hljs-type">SQLDatabase</span> {
        <span class="hljs-keyword">return</span> db <span class="hljs-keyword">as</span>! <span class="hljs-type">SQLDatabase</span>
    }
}
</code></pre>
<h3 id="heading-public-files-for-domain-verification">Public Files for Domain Verification</h3>
<p>iOS and Android each require a file to be hosted so that your domain can be verified and associated with your app. The files need to be available publicly, in a <code>.well-known</code> directory. In order to expose these files publicly, you’ll need to include this in your <code>configure.swift</code> file, in the <code>configure(app: Application)</code> function:</p>
<pre><code class="lang-swift">app.middleware.use(<span class="hljs-type">FileMiddleware</span>(publicDirectory: app.directory.publicDirectory))
</code></pre>
<h3 id="heading-apples-apple-app-site-association-file">Apple’s apple-app-site-association File</h3>
<p>The <code>apple-app-site-association</code> file, or AASA, is a JSON file that include’s your app’s identifier, including team ID. Include this code in your routes.swift:</p>
<pre><code class="lang-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">routes</span><span class="hljs-params">(<span class="hljs-number">_</span> app: Application)</span></span> <span class="hljs-keyword">throws</span> {
    app.<span class="hljs-keyword">get</span>(“.well-known”, “apple-app-site-association”) { req -&gt; <span class="hljs-type">Response</span> <span class="hljs-keyword">in</span>
        <span class="hljs-keyword">let</span> appIdentifier = “teamId.appIdentifier”
        <span class="hljs-keyword">let</span> responseString =
            “””
            {
                “applinks”: {
                    “details”: [
                        {
                            “appIDs”: [
                                “\(appIdentifier)”
                            ],
                            “components”: [
                            ]
                        }
                    ]
                },
                “webcredentials”: {
                    “apps”: [
                        “\(appIdentifier)”
                    ]
                }
            }
            “””

        <span class="hljs-keyword">let</span> response = <span class="hljs-keyword">try</span> await responseString.encodeResponse(<span class="hljs-keyword">for</span>: req)
        response.headers.contentType = <span class="hljs-type">HTTPMediaType</span>(type: “application”, subType: “json”)
        <span class="hljs-keyword">return</span> response
    }
}
</code></pre>
<p>Your Xcode project must also have a corresponding setup. You’ll need to configure Associated Domains in the project settings. Here’s a link to the <a target="_blank" href="https://developer.apple.com/documentation/xcode/supporting-associated-domains">official documentation</a>. We'll cover that more in the iOS tutorial.</p>
<h3 id="heading-androids-assetlinksjson-file">Android’s assetlinks.json File</h3>
<p>Android requires a similar file to be hosted, the <code>assetlinks.json</code> file. This file requires your android app’s identifier, as well as a SHA256 certificate fingerprint for the app. Once you’ve created a keystore for your android app, navigate to your project’s directory in terminal and run this command to generate the fingerprint:</p>
<p><code>keytool -list -v -keystore PasskeysAndroidKeystore.jks</code></p>
<p>This is what you’ll need to include in your <code>routes.swift</code> file. <code>packageName</code> is your android app identifier:</p>
<pre><code class="lang-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">routes</span><span class="hljs-params">(<span class="hljs-number">_</span> app: Application)</span></span> <span class="hljs-keyword">throws</span> {
app.<span class="hljs-keyword">get</span>(“.well-known”, “assetlinks.json”) { req -&gt; <span class="hljs-type">Response</span> <span class="hljs-keyword">in</span>
        <span class="hljs-keyword">let</span> packageName = “com.example.passkeys_android”
        <span class="hljs-keyword">let</span> responseString =
        “””
        [
            {
                “relation” : [
                    “delegate_permission/common.handle_all_urls”,
                    “delegate_permission/common.get_login_creds”
                ],
                “target” : {
                    “namespace” : “android_app”,
                    “package_name” : “\(packageName)”,
                    “sha256_cert_fingerprints” : [
                        “<span class="hljs-type">YOUR</span>:<span class="hljs-type">SHA</span>:<span class="hljs-type">FINGERPRINT</span>:<span class="hljs-type">GOES</span>:<span class="hljs-type">HERE</span>”
                    ]
                }
            }
        ]
        “””

        <span class="hljs-keyword">let</span> response = <span class="hljs-keyword">try</span> await responseString.encodeResponse(<span class="hljs-keyword">for</span>: req)
        response.headers.contentType = <span class="hljs-type">HTTPMediaType</span>(type: “application”, subType: “json”)
        <span class="hljs-keyword">return</span> response
    }
}
</code></pre>
<p>More information about the assetlinks.json file can be found <a target="_blank" href="https://developer.android.com/training/app-links/verify-android-applinks">here</a>.</p>
<h3 id="heading-setting-up-webauthnmanager">Setting up WebAuthnManager</h3>
<p>This project depends on the great work done on the <a target="_blank" href="https://github.com/swift-server/swift-webauthn">swift-webauthn</a> library. As of this writing, the official 1.0 release is not out yet, but should be coming soon. WebAuthn is the official name for the standard behind Passkeys, and the swift library is an implementation of WebAuthn that is similar to what has been done for other platforms in different languages. I recommend reading up a WebAuthn a bit if you haven’t already.</p>
<p>One challenge I encountered during this project is that iOS and Android handle Passkeys differently. Android uses a different paradigm for the <code>Relying Party</code> than Apple/iOS and the rest of the web use. For this reason, we’ll need to set up 2 instances of <code>WebAuthnManager</code>in our project. One to handle the relying party for our iOS app, and another for the Android app.</p>
<p>First, you’re going to need this extension to set up those managers. I created an <code>Application+WebAuthn.swift</code> file:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">import</span> Vapor
<span class="hljs-keyword">import</span> WebAuthn

<span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">Application</span> </span>{
    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">WebAuthnKey</span>: <span class="hljs-title">StorageKey</span> </span>{
        <span class="hljs-keyword">typealias</span> <span class="hljs-type">Value</span> = <span class="hljs-type">WebAuthnManager</span>
    }

    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">AndroidWebAuthnKey</span>: <span class="hljs-title">StorageKey</span> </span>{
        <span class="hljs-keyword">typealias</span> <span class="hljs-type">Value</span> = <span class="hljs-type">WebAuthnManager</span>
    }

    <span class="hljs-keyword">var</span> webAuthn: <span class="hljs-type">WebAuthnManager</span> {
        <span class="hljs-keyword">get</span> {
            <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> webAuthn = storage[<span class="hljs-type">WebAuthnKey</span>.<span class="hljs-keyword">self</span>] <span class="hljs-keyword">else</span> {
                <span class="hljs-built_in">fatalError</span>(“<span class="hljs-type">WebAuthn</span> <span class="hljs-keyword">is</span> not configured. <span class="hljs-type">Use</span> app.webAuthn”)
            }
            <span class="hljs-keyword">return</span> webAuthn
        }
        <span class="hljs-keyword">set</span> {
            storage[<span class="hljs-type">WebAuthnKey</span>.<span class="hljs-keyword">self</span>] = newValue
        }
    }

    <span class="hljs-keyword">var</span> webAuthnAndroid: <span class="hljs-type">WebAuthnManager</span> {
        <span class="hljs-keyword">get</span> {
            <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> webAuthn = storage[<span class="hljs-type">AndroidWebAuthnKey</span>.<span class="hljs-keyword">self</span>] <span class="hljs-keyword">else</span> {
                <span class="hljs-built_in">fatalError</span>(“<span class="hljs-type">WebAuthn</span> <span class="hljs-keyword">for</span> android <span class="hljs-keyword">is</span> not configured. <span class="hljs-type">Use</span> app.webAuthnAndroid”)
            }
            <span class="hljs-keyword">return</span> webAuthn
        }
        <span class="hljs-keyword">set</span> {
            storage[<span class="hljs-type">AndroidWebAuthnKey</span>.<span class="hljs-keyword">self</span>] = newValue
        }
    }
}

<span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">Request</span> </span>{    
    <span class="hljs-keyword">var</span> webAuthn: <span class="hljs-type">WebAuthnManager</span> {
        <span class="hljs-keyword">let</span> userAgent = headers[“user-agent”]
        <span class="hljs-keyword">if</span> userAgent.first?.localizedCaseInsensitiveContains(“okhttp”) ?? <span class="hljs-literal">false</span> {
            <span class="hljs-keyword">return</span> application.webAuthnAndroid
        }
        <span class="hljs-keyword">return</span> application.webAuthn
    }
}
</code></pre>
<p>Let’s talk a bit more about what’s going on in this extension. Passkeys for the web and iOS use a URL, referred to as the <code>Relying Party</code> in Passkey terminology. Your iOS or web app might have a relying party of <code>example.com</code> or <code>login.example.com</code>, and when the Passkey authentication dance happens, your server will verify the origin of the passkey, which will be <code>https://www.example.com</code>.</p>
<p>However, Android apps using the <code>CredentialManager</code> API use a different relying party origin format:</p>
<p><code>android:apk-key-hash:&lt;sha256-hash-for-your-app's-apk-signing-cert&gt;</code></p>
<p>Since these formats are completely different, a one-size-fits-all approach doesn't exist for iOS and Android. We have to use 2 different instances of <code>WebAuthnManager</code>, and look at the request's user agent header to determine if the request came from Android so we can handle Android's unique format. As you can see above, I've built this into an extension on Vapor's <code>Request</code> so we can easily get the appropriate manager when handling routes.</p>
<blockquote>
<h3 id="heading-side-note-ngrok">Side Note -- ngrok</h3>
<p>When you're ready to test this out with an iOS or Android app, you're going to need a way to call this server's endpoints with https. If you're just hosting it locally, that's not going to work. Enter <a target="_blank" href="https://ngrok.com">ngrok</a>. This tool creates an http or https endpoint and routes the traffic to your local machine, which means you can run this server on your local machine and reach its endpoints both using https and from physical mobile devices (Android will require a physical device, but the iOS simulator will work).</p>
<p>To get started, you'll need to create a free ngrok account and set it up. Then you can follow the instructions <a target="_blank" href="https://download.ngrok.com/downloads/mac-os">here</a> to configure ngrok on your machine. Here's the command I use to run it after it's set up:</p>
<pre><code class="lang-bash">./ngrok-2 http 8080
</code></pre>
<p>This will create a url that forwards to your <a target="_blank" href="http://localhost">localhost</a> on port 8080. This is the default port for running vapor locally in Xcode.</p>
</blockquote>
<h4 id="heading-adding-the-ngrok-url-to-your-environment-properties">Adding the ngrok url to your environment properties</h4>
<p>If everything is set up correctly, ngrok will create a url that looks something like this:</p>
<p><code>https://5e55-2000-1300-3244-89c0-q049-7106-da49-535g.ngrok-free.app</code></p>
<p>The full url will be your relying party origin, and the domain without https is your relying party id:</p>
<pre><code class="lang-plaintext">RP_ID=e55-2000-1300-3244-89c0-q049-7106-da49-535g.ngrok-free.app
RP_ORIGIN=https://e55-2000-1300-3244-89c0-q049-7106-da49-535g.ngrok-free.app
</code></pre>
<p>You'll need to restart the server after setting environment variables for them to take effect.</p>
<h2 id="heading-passkey-creation-sign-up">Passkey Creation / Sign Up</h2>
<p>Current best practice is to require a user to have an account and be authenticated before you allow them to create a Passkey. If you have an existing application, you can offer Passkey creation after a user has signed in. If you're creating something new, some non-Passkey form of account creation and authentication will also need to be created. To keep this project tightly scoped to Passkeys, and provide a working example without getting even deeper into the details, I am not implementing a full login sequence.</p>
<p>Also note that this would also require some of the steps below to be slightly different. A user setting up a passkey would already have a user name and user ID, and you'd know what they are at the time of Passkey creation. You'll want to verify their login credentials before creating a Passkey. You wouldn't want to just let anyone create a Passkey without being authenticated first.</p>
<h3 id="heading-handling-the-first-sign-up-step-the-user-challenge">Handling the first sign up step, the user challenge</h3>
<p>Users of your client applications will need to create a Passkey the first before they can start authenticating with one. To get the process started, clients will make a GET request to your <code>/signup</code> endpoint:</p>
<pre><code class="lang-swift">app.<span class="hljs-keyword">get</span>(<span class="hljs-string">"signup"</span>, use: { req -&gt; <span class="hljs-type">ChallengeResponse</span> <span class="hljs-keyword">in</span>
    <span class="hljs-comment">// 1. the endpoint requires username as a query param</span>
    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> username: <span class="hljs-type">String</span> = req.query[<span class="hljs-string">"username"</span>] <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> <span class="hljs-type">Abort</span>(.badRequest, reason: <span class="hljs-string">"Username must be supplied."</span>)
    }

    <span class="hljs-comment">// 2. check to make sure we don't already have that username in the DB</span>
    <span class="hljs-keyword">let</span> foundUser = <span class="hljs-keyword">try</span> await req.mySQL
        .raw(<span class="hljs-type">SQLQueryString</span>(<span class="hljs-string">"SELECT username FROM user WHERE username = \(bind: username) LIMIT 1"</span>))
        .first(decoding: <span class="hljs-type">UserResult</span>.<span class="hljs-keyword">self</span>)

    <span class="hljs-keyword">guard</span> foundUser == <span class="hljs-literal">nil</span> <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> <span class="hljs-type">Abort</span>(.conflict, reason: <span class="hljs-string">"Username already taken."</span>)
    }

    <span class="hljs-comment">// 3. create a new user credential for the requesting user</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">try</span> await makeUserCredential(<span class="hljs-keyword">for</span>: username, req: req)
})
</code></pre>
<p>Here's what's going on above:</p>
<ol>
<li><p>We grab the <code>username</code> query param required by the <code>/signup</code> endpoint</p>
</li>
<li><p>We check to make sure that username doesn't already exist. If it does, this user can't sign up again. If this is a new unique user, we move to step 3</p>
</li>
<li><p>We'll return a <code>ChallengeResponse</code>. This is the result of calling <code>makeUserCredential</code>, which we'll build next:</p>
</li>
</ol>
<pre><code class="lang-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">makeUserCredential</span><span class="hljs-params">(<span class="hljs-keyword">for</span> username: String, req: Request)</span></span> async <span class="hljs-keyword">throws</span> -&gt; <span class="hljs-type">ChallengeResponse</span> {

    <span class="hljs-comment">// 1. Create details for the new user and insert into the DB</span>
    <span class="hljs-keyword">let</span> newUserId = <span class="hljs-type">UUID</span>().uuidString
    <span class="hljs-keyword">let</span> timestamp = <span class="hljs-type">Date</span>()
    <span class="hljs-keyword">try</span> await req.mySQL
        .raw(<span class="hljs-type">SQLQueryString</span>(<span class="hljs-string">"INSERT INTO user(id, username, creation_date) VALUES(\(bind: newUserId), \(bind: username), \(bind: timestamp))"</span>))
        .run()

    <span class="hljs-comment">// 2. Create a new user and generate a challenge</span>
    <span class="hljs-keyword">let</span> user = <span class="hljs-type">User</span>(id: newUserId,
                    username: username,
                    creationDate: timestamp)

    <span class="hljs-keyword">let</span> options = req.webAuthn.beginRegistration(user: user.webAuthnUser)
    <span class="hljs-keyword">let</span> challenge = <span class="hljs-type">Data</span>(options.challenge).base64EncodedString()

    <span class="hljs-comment">// 3. Save the challenge to the DB and return it back to the client app</span>
    <span class="hljs-keyword">try</span> await req.mySQL
        .raw(<span class="hljs-type">SQLQueryString</span>(<span class="hljs-string">"INSERT INTO userchallenge(user_id, challenge) VALUES(\(bind: newUserId), \(bind: challenge))"</span>))
        .run()

    <span class="hljs-keyword">let</span> response = <span class="hljs-type">ChallengeResponse</span>(challenge: options,
                                     userId: newUserId)
    <span class="hljs-keyword">return</span> response
}
</code></pre>
<p>Here's a rundown on what's going on in <code>/makeUserCredential</code>:</p>
<ol>
<li><p>First we create an internal user ID for our new user, and we'll also save their account creation date. You'd already have this for an existing account, but maybe you'd want to track the date of when they created a Passkey. Anyway, we insert the new record into our <code>user</code> table.</p>
</li>
<li><p>Create a new <code>User</code> object and use that to generate a <code>PublicKeyCredentialUserEntity</code> that we feed into the WebAuthn registration function to create a <code>PublicKeyCredentialCreationOptions</code>. The <code>challenge</code> is the key piece of information here. We have to convert the byte array to a base 64 string.</p>
</li>
<li><p>Finally, we save the challenge data to the database with the user's ID, and send this back in the response to the client app.</p>
</li>
</ol>
<p>At this point, the client app will receive this challenge data and use it to generate a Passkey. In the next step, we'll receive the Passkey from the client in a <code>/makeCredential</code> endpoint and finish the Passkey registration process.</p>
<h3 id="heading-handling-the-second-sign-up-step-creating-a-credential">Handling the second sign up step, creating a credential</h3>
<p>The next endpoint we need to implement is the <code>/makeCredential</code> endpoint (not to be confused with <code>makeUserCredential</code> above in the previous step. Notice that this is a POST endpoint. The post body is a <code>RegistrationCredential</code>, which is part of the Swift WebAuthn library in our project. This object gets created by the client app when it creates the user's Passkey.</p>
<pre><code class="lang-swift">app.post(<span class="hljs-string">"makeCredential"</span>) { req -&gt; <span class="hljs-type">HTTPStatus</span> <span class="hljs-keyword">in</span>

    <span class="hljs-comment">// 1. Find the user we're registering a credential for</span>
    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> userId: <span class="hljs-type">String</span> = req.query[<span class="hljs-string">"userId"</span>] <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> <span class="hljs-type">Abort</span>(.badRequest, reason: <span class="hljs-string">"The user id must be supplied."</span>)
    }

    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> user = <span class="hljs-keyword">try</span> await req.mySQL
        .raw(<span class="hljs-type">SQLQueryString</span>(<span class="hljs-string">"SELECT * from user WHERE id = \(bind: userId)"</span>))
        .first(decoding: <span class="hljs-type">User</span>.<span class="hljs-keyword">self</span>) <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">throw</span> <span class="hljs-type">Abort</span>(.badRequest, reason: <span class="hljs-string">"UserId must be supplied."</span>)
    }

    <span class="hljs-comment">// 2. Obtain the challenge we stored on the server for this session</span>
    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> challenge = <span class="hljs-keyword">try</span> await req.mySQL
        .raw(<span class="hljs-type">SQLQueryString</span>(<span class="hljs-string">"SELECT * from userchallenge WHERE user_id = \(bind: userId)"</span>))
        .first(decoding: <span class="hljs-type">ChallengeResult</span>.<span class="hljs-keyword">self</span>),
          <span class="hljs-keyword">let</span> challengeData = <span class="hljs-type">Data</span>(base64Encoded: challenge.challenge.urlEncoded.base64String()) <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">throw</span> <span class="hljs-type">Abort</span>(.badRequest, reason: <span class="hljs-string">"Missing registration session id."</span>)
    }

    <span class="hljs-comment">// 3. Delete the challenge from the server to prevent an attacker from reusing it</span>
    <span class="hljs-keyword">try</span> await req.mySQL.raw(<span class="hljs-type">SQLQueryString</span>(<span class="hljs-string">"DELETE FROM userchallenge WHERE user_id = \(bind: userId)"</span>)).run()

    <span class="hljs-comment">// 4. Verify the credential the client sent us</span>
    <span class="hljs-keyword">let</span> credential = <span class="hljs-keyword">try</span> await req.webAuthn.finishRegistration(
        challenge: [<span class="hljs-type">UInt8</span>](challengeData),
        credentialCreationData: req.content.decode(<span class="hljs-type">RegistrationCredential</span>.<span class="hljs-keyword">self</span>, <span class="hljs-keyword">as</span>: .json),
        confirmCredentialIDNotRegisteredYet: { credentialId <span class="hljs-keyword">in</span>
            <span class="hljs-keyword">let</span> existingCredential = <span class="hljs-keyword">try</span> await req.mySQL
                .raw(<span class="hljs-type">SQLQueryString</span>(<span class="hljs-string">"SELECT * FROM usercredential WHERE credential_id = \(bind: credentialId)"</span>))
                .first(decoding: <span class="hljs-type">CredentialResult</span>.<span class="hljs-keyword">self</span>)
            <span class="hljs-keyword">return</span> existingCredential == <span class="hljs-literal">nil</span>
        }
    )

    <span class="hljs-comment">// 5. If the credential was verified, save it to the database</span>
    <span class="hljs-keyword">let</span> authnCredential = <span class="hljs-type">WebAuthnCredential</span>(from: credential, userId: user.id)
    <span class="hljs-keyword">try</span> await req.mySQL
        .raw(<span class="hljs-type">SQLQueryString</span>(<span class="hljs-string">"INSERT INTO usercredential(user_id, credential_id, public_key) VALUES(\(bind: user.id), \(bind: authnCredential.id), \(bind: authnCredential.publicKey))"</span>))
        .run()

    <span class="hljs-keyword">return</span> .ok
}
</code></pre>
<p>There's a going on here, but it's not too hard to understand. Let's break it down:</p>
<ol>
<li><p>The client app sends us the user id we created in the previous step so we can identify the user and make sure we already know about them.</p>
</li>
<li><p>Now we use that user id to grab the user's challenge data that we stored during the first stage of Passkey registration in the previous endpoint's implementation. We're going to take that challenge string, url encode it, base 64 encode it, and convert it to <code>Data</code>.</p>
<ol>
<li>Note: Get used to base 64 and url encoded strings. This is going to be a really common theme throughout a Passkey implementation.</li>
</ol>
</li>
<li><p>Now that we have our <code>challengeData</code> stored in memory, we're going to delete that challenge from the database. This ensures that the challenge can't be used again by an attacker.</p>
</li>
<li><p>There's a lot packed into this step. The first param of the WebAuthn <code>finishRegistration</code> function is our <code>challengeData</code> converted to a byte array. We're also decoding the post body of the request into a <code>RegistrationCredential</code> and making sure that credential's id doesn't already exist in our DB with the <code>confirmCredentialIDNotRegisteredYet</code> closure. If everything succeeds, the credential is verified and we'll get a <code>Credential</code> object.</p>
</li>
<li><p>Now we create a homemade object, <code>WebAuthnCredential</code>, and we'll use that to help save our userId, credentialId, and publicKey to the database. If everything succeeds, we'll just return a 200 ok response to the client app.</p>
</li>
</ol>
<p>Phew! Registration is now complete.</p>
<h2 id="heading-passkey-authentication">Passkey Authentication</h2>
<h3 id="heading-generating-an-auth-challenge">Generating an Auth Challenge</h3>
<p>You'll need to create a <code>GET</code> endpoint named <code>/authentication</code>. It takes a <code>username</code> field as a query param, which will most likely be the user's email address.</p>
<pre><code class="lang-swift">app.<span class="hljs-keyword">get</span>(<span class="hljs-string">"authenticate"</span>) { req -&gt; <span class="hljs-type">PublicKeyCredentialRequestOptions</span> <span class="hljs-keyword">in</span>
    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> username: <span class="hljs-type">String</span> = req.query[<span class="hljs-string">"username"</span>] <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> <span class="hljs-type">Abort</span>(.badRequest, reason: <span class="hljs-string">"Username must be supplied."</span>)
    }

    <span class="hljs-comment">// 1. generate a challenge for the user attempting to log in</span>
    <span class="hljs-keyword">let</span> options = <span class="hljs-keyword">try</span> req.webAuthn.beginAuthentication()
    <span class="hljs-keyword">let</span> optionDataString = <span class="hljs-type">Data</span>(options.challenge).base64EncodedString()

    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> existingUser = <span class="hljs-keyword">try</span> await req.mySQL
        .raw(<span class="hljs-type">SQLQueryString</span>(<span class="hljs-string">"SELECT * FROM user WHERE username = \(bind: username)"</span>))
        .first(decoding: <span class="hljs-type">User</span>.<span class="hljs-keyword">self</span>) <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> <span class="hljs-type">Abort</span>(.badRequest, reason: <span class="hljs-string">"User not found."</span>)
    }

    <span class="hljs-comment">// 2. save that challenge to the database as a base64 encoded string</span>
    <span class="hljs-keyword">try</span> await req.mySQL
        .raw(<span class="hljs-type">SQLQueryString</span>(<span class="hljs-string">"INSERT INTO userauthchallenge(user_id, challenge) VALUES(\(bind: existingUser.id), \(bind: optionDataString))"</span>))
        .run()

    <span class="hljs-comment">// 3. return the generated credential request options back to the caller</span>
    <span class="hljs-keyword">return</span> options
}
</code></pre>
<p>This is all pretty straightforward:</p>
<ol>
<li><p>After making sure we received a username as a query param, we use our <code>Request</code> extension created earlier to get the right <code>WebAuthnManager</code> instance and call <code>beginAuthentication()</code>. This generates a challenge for this authentication attempt. We also convert it to a base64 encoded string so we can save it to the database.</p>
</li>
<li><p>Save that challenge to the database, along with the user's ID</p>
</li>
<li><p>Return the <code>PublicKeyCredentialRequestOptions</code> back to the caller so they can perform the next authentication step.</p>
</li>
</ol>
<h3 id="heading-finishing-authentication">Finishing Authentication</h3>
<p>Next we'll create a <code>POST</code> endpoint called <code>/authenticate</code>. We're also taking the username as a query parameter again, and also require an <code>AuthenticationCredential</code> as a post body. Here's the implementation:</p>
<pre><code class="lang-swift">app.post(<span class="hljs-string">"authenticate"</span>) { req -&gt; <span class="hljs-type">HTTPStatus</span> <span class="hljs-keyword">in</span>
    <span class="hljs-comment">// 1. Get the user from the query param and make sure we know who it is</span>
    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> username: <span class="hljs-type">String</span> = req.query[<span class="hljs-string">"username"</span>] <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> <span class="hljs-type">Abort</span>(.badRequest, reason: <span class="hljs-string">"Username must be supplied."</span>)
    }

    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> existingUser = <span class="hljs-keyword">try</span> await req.mySQL
        .raw(<span class="hljs-type">SQLQueryString</span>(<span class="hljs-string">"SELECT * FROM user WHERE username = \(bind: username)"</span>))
        .first(decoding: <span class="hljs-type">User</span>.<span class="hljs-keyword">self</span>) <span class="hljs-keyword">else</span> {

        <span class="hljs-keyword">throw</span> <span class="hljs-type">Abort</span>(.badRequest, reason: <span class="hljs-string">"User not found."</span>)
    }

    <span class="hljs-comment">// 2. Make sure we have a challenge saved for this user</span>
    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> challenge = <span class="hljs-keyword">try</span> await req.mySQL
        .raw(<span class="hljs-type">SQLQueryString</span>(<span class="hljs-string">"SELECT * from userauthchallenge WHERE user_id = \(bind: existingUser.id)"</span>))
        .first(decoding: <span class="hljs-type">ChallengeResult</span>.<span class="hljs-keyword">self</span>),
          <span class="hljs-keyword">let</span> challengeData = <span class="hljs-type">Data</span>(base64Encoded: challenge.challenge.urlEncoded.base64String()) <span class="hljs-keyword">else</span> {

        <span class="hljs-keyword">try</span> await removeAuthChallenge(<span class="hljs-keyword">for</span>: existingUser.id, req: req)
        <span class="hljs-keyword">throw</span> <span class="hljs-type">Abort</span>(.badRequest, reason: <span class="hljs-string">"Missing auth session id."</span>)
    }

    <span class="hljs-comment">// 3. Delete the challenge from the server to prevent an attacker from reusing it</span>
    <span class="hljs-keyword">try</span> await removeAuthChallenge(<span class="hljs-keyword">for</span>: existingUser.id, req: req)

    <span class="hljs-comment">// 4. Decode the credential the client sent us</span>
    <span class="hljs-keyword">var</span> authenticationCredential = <span class="hljs-keyword">try</span> req.content.decode(<span class="hljs-type">AuthenticationCredential</span>.<span class="hljs-keyword">self</span>)
    <span class="hljs-keyword">let</span> authenticatorData = authenticationCredential.response.authenticatorData
    <span class="hljs-keyword">let</span> authString = <span class="hljs-type">String</span>(bytes: authenticatorData, encoding: .utf8)

    <span class="hljs-comment">// 5. Make sure we have an auth credential saved in our database for this user</span>
    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> foundCredential = <span class="hljs-keyword">try</span> await req.mySQL
        .raw(<span class="hljs-type">SQLQueryString</span>(<span class="hljs-string">"SELECT * FROM usercredential WHERE credential_id = \(bind: authenticationCredential.id.urlDecoded)"</span>))
        .first(decoding: <span class="hljs-type">UserAuthCredential</span>.<span class="hljs-keyword">self</span>) <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">try</span>? await removeAuthChallenge(<span class="hljs-keyword">for</span>: existingUser.id, req: req)
            <span class="hljs-keyword">throw</span> <span class="hljs-type">Abort</span>(.badRequest)
    }

    <span class="hljs-keyword">let</span> credentialPublicKey = <span class="hljs-type">URLEncodedBase64</span>(foundCredential.publicKey).urlDecoded
    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> decodedPublicKey = credentialPublicKey.decoded <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">try</span>? await removeAuthChallenge(<span class="hljs-keyword">for</span>: existingUser.id, req: req)
        <span class="hljs-keyword">throw</span> <span class="hljs-type">Abort</span>(.badRequest)
    }

    <span class="hljs-comment">// 6. If we found a credential, use the stored public key to verify the challenge</span>
    <span class="hljs-comment">// sign count will always be 0, we don't need to do anything else with it</span>
    <span class="hljs-number">_</span> = <span class="hljs-keyword">try</span> req.webAuthn.finishAuthentication(
        credential: authenticationCredential,
        expectedChallenge: [<span class="hljs-type">UInt8</span>](challengeData),
        credentialPublicKey: [<span class="hljs-type">UInt8</span>](decodedPublicKey),
        credentialCurrentSignCount: <span class="hljs-number">0</span>)

    <span class="hljs-keyword">return</span> .ok
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">removeAuthChallenge</span><span class="hljs-params">(<span class="hljs-keyword">for</span> userId: String, req: Request)</span></span> async <span class="hljs-keyword">throws</span> {
    <span class="hljs-keyword">try</span> await req.mySQL.raw(<span class="hljs-type">SQLQueryString</span>(<span class="hljs-string">"DELETE FROM userauthchallenge WHERE user_id = \(bind: userId)"</span>)).run()
}
</code></pre>
<p>Ok, this is a bit of a longer function, but hopefully things are coming together in a way that makes sense:</p>
<ol>
<li><p>First we're just making sure the client sent a username and that we know enough about that username to do something with it</p>
</li>
<li><p>We also need to make sure we've previously saved a challenge for this user. We need to decode that back to <code>Data</code> from it's base64 url encoded format we saved to the database.</p>
</li>
<li><p>Similar to some previous spots, we're going to remove this challenge from our database now. If an attacker attempted a replay attack, we won't fall for it because we've already had a request come through for this user and we've cleaned up.</p>
</li>
<li><p>It's time to decode the <code>AuthenticationCredential</code> that was sent in the payload of this request. In particular, we're looking for the <code>authenticatorData</code> piece, and we convert that to a String.</p>
</li>
<li><p>Now we check if we've previously saved a <code>UserAuthCredential</code> to the database for this user. This contains the public key we'll use to validate the user's credential. Again, if we don't find anything we stored previously in the database, we're deleting the challenge for this user. If anything smells off, we err on the side of caution. Worst case, a legit user will get a failure and can just try again.</p>
</li>
<li><p>The WebAuthn library validates the credential. If everything lines up, success!</p>
</li>
</ol>
<p>At this point, instead of sending an empty 200 response, you'd most likely want to send a token in the response that your user will pass with every subsequent request, granting them access to your server's endpoints until the token expires. If you're adding Passkeys to an existing application, you probably already have this in place. If you're starting from scratch, <code>REST API token management</code> would be a great next rabbit hole to go down.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Thanks for reading this far...we've covered a lot! Hopefully I've given you enough information to dive deeper on any areas that aren't as clear or you are interested in learning more about.</p>
<p>This is the first in a series of articles on implementing a Passkey authentication server that can serve iOS and Android mobile apps. The other 2 articles can be found here:</p>
<ul>
<li><p>iOS: <a target="_blank" href="https://whiterockstudios.hashnode.dev/creating-a-passkey-client-for-ios">https://whiterockstudios.hashnode.dev/creating-a-passkey-client-for-ios</a></p>
</li>
<li><p>Android: <a target="_blank" href="https://whiterockstudios.hashnode.dev/creating-a-passkey-client-for-android">https://whiterockstudios.hashnode.dev/creating-a-passkey-client-for-android</a></p>
</li>
</ul>
<h3 id="heading-sources">Sources</h3>
<p>The following articles were helpful or served as inspiration:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/brokenhandsio/Vapor-PasskeyDemo">Broken Hands VaporPasskeys-Demo</a></p>
</li>
<li><p><a target="_blank" href="https://w3c.github.io/webauthn">W3C WebAuthn</a></p>
</li>
</ul>
]]></content:encoded></item></channel></rss>