FIDO2 web guide

This guide explains how to implement the FIDO2 registration and authentication workflow on your Web applications. You’ll find a detailed introduction about FIDO2 in our FIDO2 guide.

Prerequisites

  • The WebAuthn feature must be enabled on your ReachFive account.

You may need to contact support to have this enabled if you do not see it on your ReachFive Console.
You also need

A device with a fingerprint sensor or a configured screen lock.

Make sure to register a fingerprint (or screen lock).

Instructions

  1. Log in to your ReachFive Console.

  2. Go to Settings > WebAuthn.

  3. Configure the following:

    console webauthn settings
    1. Application name: the name of the application that is displayed to the user during the FIDO2 Registration or Authentication process. There are no particular restrictions on what you can use here.

    2. Allowed origins: the list of URLs that are allowed to make calls to the API endpoints provided by ReachFive to perform the FIDO2 Registration or Authentication process.

You should first check the browser supports public-key credentials:

if (window.PublicKeyCredential) {
    // Code here
}
else {
    return new Error('Unsupported WebAuthn API')
}

Sign up

The user requests to register a new account for the first time. Users don’t need to provide a password, but instead provide an identifier like an email or mobile number as well as some personal data.

  1. We initiate the FIDO2 signup process by retrieving a randomly generated challenge, the Relying Party information, and the user information from the ReachFive server with the POST /identity/v1/webauthn/signup-options endpoint.

    // Return a promise fulfilled with the signup options
    function createSignupOptions(userParams) {
        const params = {
            origin: window.location.origin,
            friendlyName: userParams.friendlyName || window.navigator.platform,
            profile: userParams.profile,
            clientId: yourClientId,
            scope: yourScope,
            redirectUrl: yourRedirectUrl,
            returnToAfterEmailConfirmation: yourReturnToAfterEmailConfirmation
          }
    
        return fetch('https://${YOUR_DOMAIN}/identity/v1/webauthn/signup-options', { (1)
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(params)
        })
    }
  2. We pass the signup options to the Credential Management API (navigator.credentials.create()) which will open a fingerprint dialog and generate a new credential.

    credentials.create()
    // Return a promise fulfilled with a new credential
    function createCredentials(signupOptions) {
        const serializedOptions = signupOptions.options.publicKey
    
        const publicKey = {
            ...serializedOptions,
            challenge: Buffer.from(serializedOptions.challenge, 'base64'),
            user: {
                ...serializedOptions.user,
                id:  Buffer.from(serializedOptions.user.id, 'base64')
            },
            excludeCredentials: serializedOptions.excludeCredentials && serializedOptions.excludeCredentials!.map(excludeCredential => ({
                ...excludeCredential,
                id: Buffer.from(excludeCredential.id, 'base64')
            }))
        }
    
        return navigator.credentials.create({ publicKey })
    }
  3. With the POST /identity/v1/webauthn/signup endpoint, we send the new credential back to the ReachFive server and it finalizes the FIDO2 signup process by returning a one-time authentication token.

    import { Buffer } from 'buffer/'
    
    // Encode an array into Base64 url safe
    function encodeToBase64(array) {
        return Buffer.from(array)
            .toString('base64')
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=+$/, '')
    }
    
    // Return a promise fulfilled with a one-time authentication token
    function signupWithWebauthn(signupOptions, credentials) {
        if (!credentials || credentials.type !== 'public-key') {
            return new Error('Unable to register invalid public key credentials.')
        }
    
        const serializedCredentials = {
            id: credentials.id,
            rawId: encodeToBase64(credentials.rawId),
            type: credentials.type,
            response: {
                clientDataJSON: encodeToBase64(credentials.response.clientDataJSON),
                attestationObject: encodeToBase64(credentials.response.attestationObject)
            }
        }
    
        return fetch('https://${YOUR_DOMAIN}/identity/v1/webauthn/signup', { (3)
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                publicKeyCredential: serializedCredentials,
                webauthnId: signupOptions.options.publicKey.user.id
            })
        })
    }
  4. Finally, we call the GET /oauth/authorize endpoint to parse the result of the signup request and retrieve the user’s authentication token.

    // Login the user and redirects to the URL specified as `redirect_uri`
    function authorizeUser(tkn) {
        const location = `https://${YOUR_DOMAIN}/oauth/authorize?` + (4)
            `client_id=${YOUR_CLIENT_ID}` +
            `&scope=${YOUR_SCOPE}` +
            `&response_type=code` +
            `&redirect_uri=${YOUR_REDIRECT_URI}` +
            `&code_challenge=${YOUR_CODE_CHALLENGE}` +
            `&code_challenge_method=S256` +
            `&tkn=${oneTimeAuthenticationToken}`
    
        window.location.assign(location)
    }

Authenticate

Once the user has registered their credentials on the server and the device, they can use it to easily login with their identifier.

  1. We initiate the FIDO2 authentication process to retrieve the list of previously registered credentials and a challenge string from the server with the POST /identity/v1/webauthn/authentication-options endpoint.

    // Return a promise fulfilled with the authentication options
    function createAuthenticationOptions(userParams) {
        const params = {
            email: userParams.email,
            phoneNumber: userParams.phoneNumber,
            origin: window.location.origin,
            clientId: yourClientId,
            scope: yourScope
          }
    
        return fetch('https://${YOUR_DOMAIN}/identity/v1/webauthn/authentication-options', { (1)
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(params)
        })
    }
  2. This information is passed to the Credential Management API (navigator.credentials.get()) which searches for a credential that matches the Relying Party ID. A fingerprint dialog is opened for the user to consent for authentication and it retrieves the existing credential.

    credentials.get()
    // Return a promise fulfilled with an existing credential
    function getCredentials(authenticationOptions) {
        const serializedOptions = authenticationOptions.publicKey
    
        const publicKey = {
            ...serializedOptions,
            challenge: Buffer.from(serializedOptions.challenge, 'base64'),
            allowCredentials: serializedOptions.allowCredentials.map(allowCrendential => ({
                ...allowCrendential,
                id: Buffer.from(allowCrendential.id, 'base64')
            }))
        }
    
        return navigator.credentials.get({ publicKey })
    }
  3. With the POST /identity/v1/webauthn/authentication endpoint, we send the operation result to the ReachFive server and finalizes the FIDO2 authentication process by returning a one-time authentication token.

    import { Buffer } from 'buffer/'
    
    // Encode an array into Base64 url safe
    function encodeToBase64(array) {
        return Buffer.from(array)
            .toString('base64')
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=+$/, '')
    }
    
    // Return a promise fulfilled with a one-time authentication token
    function loginWithWebauthn(credentials) {
        if (!credentials || credentials.type !== 'public-key') {
            return new Error('Unable to authenticate with invalid public key credentials.')
        }
    
        const serializedCredentials = {
            id: credentials.id,
            rawId: encodeToBase64(credentials.rawId),
            type: credentials.type,
            response: {
                authenticatorData: encodeToBase64(credentials.response.authenticatorData),
                clientDataJSON: encodeToBase64(credentials.response.clientDataJSON),
                signature: encodeToBase64(credentials.response.signature),
                userHandle: credentials.response.userHandle && encodeToBase64(credentials.response.userHandle)
            }
        }
    
        return fetch('https://${YOUR_DOMAIN}/identity/v1/webauthn/authentication', { (3)
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ ...serializedCredentials })
        })
    }
  4. Finally, we call the GET /oauth/authorize endpoint to parse the result of the authentication request and retrieve the user’s authentication token.

    // Login the user and redirects to the URL specified as `redirect_uri`
    function authorizeUser(tkn) {
        const location = `https://${YOUR_DOMAIN}/oauth/authorize?` + (4)
            `client_id=${YOUR_CLIENT_ID}` +
            `&scope=${YOUR_SCOPE}` +
            `&response_type=code` +
            `&redirect_uri=${YOUR_REDIRECT_URI}` +
            `&code_challenge=${YOUR_CODE_CHALLENGE}` +
            `&code_challenge_method=S256` +
            `&tkn=${oneTimeAuthenticationToken}`
    
        window.location.assign(location)
    }

Register

Once the user is authenticated, they can register their new device. Hooray!

A user cannot register several credentials on the same device.
  1. We initiate the FIDO2 registration process by retrieving a randomly generated challenge, the Relying Party information, and the user information from the ReachFive server with the POST /identity/v1/webauthn/registration-options endpoint.

    // Return a promise fulfilled with the registration options
    function createRegistrationOptions(accessToken, friendlyName) {
        const params = {
            friendlyName: friendlyName || window.navigator.platform,
            origin: window.location.origin,
          }
    
        return fetch('https://${YOUR_DOMAIN}/identity/v1/webauthn/registration-options', { (1)
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${accessToken}`
            },
            body: JSON.stringify(params)
        })
    }
  2. We pass the registration options to the Credential Management API (navigator.credentials.create()) which will open a fingerprint dialog and generate a new credential.

    credentials.create()
    // Return a promise fulfilled with a new credential
    function createCredentials(regisationOptions) {
        const serializedOptions = registrationOptions.options.publicKey
    
        const publicKey = {
            ...serializedOptions,
            challenge: Buffer.from(serializedOptions.challenge, 'base64'),
            user: {
                ...serializedOptions.user,
                id:  Buffer.from(serializedOptions.user.id, 'base64')
            },
            excludeCredentials: serializedOptions.excludeCredentials && serializedOptions.excludeCredentials!.map(excludeCredential => ({
                ...excludeCredential,
                id: Buffer.from(excludeCredential.id, 'base64')
            }))
        }
    
        return navigator.credentials.create({ publicKey })
    }
  3. With the POST /identity/v1/webauthn/registration endpoint, we send the operation result to the ReachFive server and completes the FIDO2 registration process.

    import { Buffer } from 'buffer/'
    
    // Encode an array into Base64 url safe
    function encodeToBase64(array) {
        return Buffer.from(array)
            .toString('base64')
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=+$/, '')
    }
    
    // Return an empty promise
    function addNewWebAuthnDevice(credentials) {
        if (!credentials || credentials.type !== 'public-key') {
            return new Error('Unable to register invalid public key credentials.')
        }
    
        const serializedCredentials = {
            id: credentials.id,
            rawId: encodeToBase64(credentials.rawId),
            type: credentials.type,
            response: {
                clientDataJSON: encodeToBase64(credentials.response.clientDataJSON),
                attestationObject: encodeToBase64(credentials.response.attestationObject)
            }
        }
    
        return fetch('https://${YOUR_DOMAIN}/identity/v1/webauthn/registration', { (3)
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${accessToken}`
            },
            body: JSON.stringify({ ...serializedCredentials })
        })
    }