Technical overview & Get started - Tutorial

Process overview

This page contains pseudo-code examples. The general concepts are covered however there are several gaps that you will need to fill for your specific implementation.
1) Attestation - server-side part 1

                $user = new User($userId)
                $challenge = array_slice(shuffle(range(1, 255)),0,64); // 64 random ints between 1 and 255
                $user->saveChallenge($challenge);

                return [
                    "challenge" => $challenge,
                    "rp" => [
                        "name" => "Your website",
                        "id" => "example.com",
                    ],
                    "user" => [
                        "id" => $user->getUuid(),
                        "name" => $user->getName(),
                        "displayName" => $user->getDisplayName(),
                    ],
                    "pubKeyCredParams" => [
                        ["alg" => -7, "type" => "public-key"],
                        ["alg" => -257, "type" => "public-key"],
                    ],
                    "authenticatorSelection" => [
                        "authenticatorAttachment" => "platform"
                    ],
                    "timeout" => 30000,
                    "attestation" => "direct"
                ];
            
2) Attestation - front-end

                var options = api.request('/get-attestation-options?userId=789');

                // you will need to convert the challenge from a string to a byte array
                options.challenge = Uint8Array.from(options.challenge);
                options.user.id = Uint8Array.from(options.user.id, c => c.charCodeAt(0));

                const credential = await navigator.credentials.create({
                    publicKey: options
                })

                // base64Url encode the credential before sending
                credential = window.prepareCredentialForApi(credential);

                api.request('/save-credential?userId=789', credential);
            
3) Attestation - server-side part 2

                $user = new User($userId);
                $credential = $requestBody['credential'];

                if ($credential->challenge == $user->getChallenge()) {
                    // you now need to be able to parse out values from the credential
                    // you will need to use libraries to decode CBOR and handle COSE keys
                    // before proceeding you also need to carry out other checks such as:
                    // credential type is webauthn.create
                    // the relying party ID in the credential matches the expected domain
                    // the user verified to create the credential
                    // the credential ID does not already exist in your database

                    $user->clearChallenge();
                    $user->addCredential($credential);

                    return ['success'];
                }
            

4) Assertion - server-side part 1

                $user = new User($userId);
                $challenge = array_slice(shuffle(range(1, 255)),0,64); // 64 random ints between 1 and 255
                $user->setChallenge($challenge);

                $allowCredentials = [];

                foreach($user->getCredentials() as $credential) {
                    $allowCredentials[] = [
                        "id" => $credential->getId(),
                        "type" => "public-key",
                        "transports" => ["internal"]
                    ]
                }

                return [
                    "challenge" => $challenge,
                    "allowCredentials" => $allowCredentials,
                    "timeout" => 60000,
                ]
            
5) Assertion - front-end

                var options = api.request('/get-assertion-options?userId=789');

                // you will need to convert these values from a string to a byte array
                options.challenge = Uint8Array.from(options.challenge);
                options.allowCredentials.forEach(element => element.id = new Uint8Array(window.base64url.decode(element.id)));

                const credential = await navigator.credentials.get({
                    publicKey: options
                });

                // base64Url encode the credential before sending
                credential = window.prepareCredentialForApi(credential);

                api.request('/verify-credential?userId=789', credential);
            
6) Assertion - server-side part 2

                $user = new User($userID)
                $challengeMatches = ($user->getChallenge() == $credential->challenge);

                // you need to again be able to parse out values from the credential
                // you will need to use libraries to decode CBOR and handle COSE keys
                $dataToVerify = // parse this out from the data in the credential
                $signature = // parse this out from the data in the credential
                $keyMatches = openssl_verify($dataToVerify, $signature, $user->getCredential($credential->id));

                if($challengeMatches && $keyMatches) {
                    $user->clearChallenge();
                    return ['success'];
                }

            
Front-end - preparing credentials for the API

                prepareCredentialForApi: async function(credential) {
                    const publicKeyCredential = {
                        id: credential.id,
                        type: credential.type,
                        rawId: window.base64url.encode(new Uint8Array(credential.rawId)),
                        response: {
                            clientDataJSON: window.base64url.encode(
                                new Uint8Array(credential.response.clientDataJSON)
                            ),
                        },
                    };

                    if (credential.response.attestationObject !== undefined) {
                        publicKeyCredential.response.attestationObject = window.base64url.encode(
                            new Uint8Array(credential.response.attestationObject)
                        );
                    }

                    if (credential.response.authenticatorData !== undefined) {
                        publicKeyCredential.response.authenticatorData = window.base64url.encode(
                            new Uint8Array(credential.response.authenticatorData)
                        );
                    }

                    if (credential.response.signature !== undefined) {
                        publicKeyCredential.response.signature = window.base64url.encode(
                            new Uint8Array(credential.response.signature)
                        );
                    }

                    if (credential.response.userHandle !== undefined) {
                        publicKeyCredential.response.userHandle = window.base64url.encode(
                            new Uint8Array(credential.response.userHandle)
                        );
                    }

                    return publicKeyCredential;
                }
            

Browser support

Web Authentication is widely supported on modern Android and iOS devices. See Can I use - Webauthn, Can I use - Credenial Management and MDN Browser compatibility for more details on browser support.

Important notes:


An easier way

If you are interested in trying out Web Authentication, the information and code samples on this site and others should give you plenty of direction to get started with. However, there is no doubt it is a complex process - in order to process the credentials your front-end will generate, you need complex server-side code to parse and verify them. However, there are tools available to help with this!

Presence is a cloud tool that is offers a ready-made Web Authentication sandbox for use in your applications and prototypes. With this tool you will receive an API key which you can use to submit and verify credentials in minutes.

Learn more about Presence - including a free trial without requiring card details.