Skip to content

Instantly share code, notes, and snippets.

@ship-happens
Last active March 24, 2025 19:14
Show Gist options
  • Select an option

  • Save ship-happens/dbc728646a051b43d5d78c916fd69f44 to your computer and use it in GitHub Desktop.

Select an option

Save ship-happens/dbc728646a051b43d5d78c916fd69f44 to your computer and use it in GitHub Desktop.
Passkey registration + authentication

Passkey registration + authentication

JS dependencies

The examples here call functions defined by the @github/webauthn-json/browser-ponyfill package (available at https://ga.jspm.io/npm:@github/webauthn-json@2.1.0/dist/esm/webauthn-json.browser-ponyfill.js).

Passkey registration flow

Registration endpoints are for signed-in users. Provide the user's JWT as usual.

To start, you need to initiate a registration challenge by making a POST to /users/passkeys/create_challenge. The response will be similar to:

{
    "challenge": "Ab0eFvIq_yqxsAqxtUABy0iRLoWJVVXrdQjyzzVeKOQ",
    "timeout": 120000,
    "extensions":
    {},
    "rp":
    {
        "name": "Displet API"
    },
    "user":
    {
        "name": "shantell3@effertz-purdy.com",
        "id": "025bf1fd-180b-42be-99f1-9bb1c782bf1d",
        "displayName": "shantell3@effertz-purdy.com"
    },
    "pubKeyCredParams":
    [
        {
            "type": "public-key",
            "alg": -7
        },
        {
            "type": "public-key",
            "alg": -37
        },
        {
            "type": "public-key",
            "alg": -257
        }
    ],
    "authenticatorSelection":
    {
        "residentKey": "required",
        "userVerification": "required"
    },
    "excludeCredentials":
    []
}

You'll then feed the JSON from the API request into two package-defined functions, parseCreationOptionsFromJSON and create:

let challengeFetch = fetch('https://api-v3.displet.com/users/passkeys/create_challenge', {
  method: "POST",
  headers: {
    "Accept": "application/json"
  }
})

const challengeJSON = await(await challengeFetch).json()
const credentialCreationOptions = parseCreationOptionsFromJSON({publicKey: challengeJSON})
const credentialCreationResponse = await create(credentialCreationOptions)

Finally, you'll submit a request to create the passkey with a POST to /users/passkeys, passing the result of JSON.stringify(credentialCreationResponse) from the example as the credential param. You can additionally pass an optional label with a friendly name that will be returned in the serialized response.

Passkey authentication flow

Authentication endpoints do not require a JWT as these will be used by users who aren't yet signed-in.

To start, you need to initiate an authentication challenge by making a POST to /users/passkeys/auth_challenge. The response will be similar to:

{
    "challenge": "j4DJ3jtHeGcZdzAeAYtmN6gBe007c1CN1JwZ1vZUq_o",
    "timeout": 120000,
    "extensions":
    {},
    "allowCredentials":
    [],
    "userVerification": "required"
}

You'll then feed the JSON from the API request into two package-defined functions, parseRequestOptionsFromJSON and get:

let challengeFetch = fetch('https://api-v3.displet.com/users/passkeys/auth_challenge', {
  method: "POST",
  headers: {
    "Accept": "application/json"
  }
})

const challengeJSON = await(await challengeFetch).json()
const credentialAuthenticationOptions = parseRequestOptionsFromJSON({publicKey: challengeJSON})
const credentialAuthenticationResponse = await get(credentialAuthenticationOptions)

Finally, you'll submit a request to authenticate the passkey with a POST to /users/passkeys/auth, passing the result of JSON.stringify(credentialAuthenticationResponse) from the example as the credential param.

Kicking off passkey authentication

It's possible to begin the authentication flow at any point - that could be following a button click, partial form completion, or on initial page load as seen at https://admin-v3.displet.com/admins/login. The Displet admin UI applies "conditional mediation" to determine if passkey authentication is an option:

class ConditionalMediationNotSupportedError extends Error {
  constructor(message) {
    super(message)
    this.name = "ConditionalMediationNotSupportedError"
  }
}

let conditionalMediationAvailable = async function(){
  if (
    typeof window.PublicKeyCredential !== 'undefined'
    && typeof window.PublicKeyCredential.isConditionalMediationAvailable === 'function'
  ) {
    return await PublicKeyCredential.isConditionalMediationAvailable()
  } else {
    return Promise.reject(new ConditionalMediationNotSupportedError('Browser does not support Conditional Mediation'))
  }
}

let startConditionalMediation = async function(form){
  const available = await conditionalMediationAvailable()
  if(!available){ return }

  // Starts passkey authentication flow
  getChallengeAndSubmitCredential(form)
}
@ship-happens
Copy link
Author

To list passkeys:
GET /users/passkeys

To delete a passkey:
DELETE /users/passkeys/:id

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment