glasslock/authentication

WebAuthn authentication ceremony.

Example (known credential)

import glasslock/authentication

// Generate options for the browser
let #(options, challenge) =
  authentication.new(
    relying_party_id: "example.com",
    origin: "https://example.com",
  )
  |> authentication.allow_credential(
    id: stored_credential.id,
    transports: stored_credential.transports,
  )
  |> authentication.build()

// Serialize `options` and send to the browser; receive
// response_json back. Keep `challenge` in memory for a single-node
// deploy; to span processes or nodes, serialize with
// `authentication.encode_challenge` and hydrate it back with
// `authentication.parse_challenge`.

// Verify the response
case authentication.verify_json(
  response_json:,
  challenge:,
  stored: stored_credential,
  user: authentication.AlreadyIdentifiedUser(account.user_handle),
) {
  Ok(updated_credential) -> todo as "update stored sign_count"
  Error(e) -> todo as "handle error"
}

Example (discoverable/passkey)

// No allow_credential calls = discoverable flow
let #(options, challenge) =
  authentication.new(
    relying_party_id: "example.com",
    origin: "https://example.com",
  )
  |> authentication.build()

// Serialize `options` and send to the browser. Parse the response,
// resolve the owning account and credential, then verify with the same
// `Response`. As above, keep `challenge` in memory for a single node, or
// round-trip through `authentication.encode_challenge` /
// `authentication.parse_challenge` to span processes.
use response <- result.try(authentication.parse_response_json(response_json))
use info <- result.try(authentication.response_info(response))
use account <- result.try(lookup_account_by_credential_id(info.credential_id))
use stored <- result.try(find_credential(account, info.credential_id))
authentication.verify(
  response:,
  challenge:,
  stored:,
  user: authentication.DiscoveredUser(account.user_handle),
)

Types

Configuration for an authentication request, built up via new and the setter functions, then handed to build to produce browser options and a challenge verifier.

pub opaque type Builder

A finalized authentication challenge ready for verification or out-of-process serialization (see encode_challenge and parse_challenge).

pub opaque type Challenge

Errors that can occur during authentication verification.

pub type Error {
  VerificationMismatch(field: glasslock.VerificationField)
  UnsupportedKey(reason: String)
  ParseError(message: String)
  InvalidSignature
  CredentialNotAllowed
  SignCountRegression
  UserPresenceFailed
  UserVerificationFailed
}

Constructors

  • VerificationMismatch(field: glasslock.VerificationField)

    A verification field does not match the expected value.

  • UnsupportedKey(reason: String)

    The key format, algorithm, or curve is not supported.

  • ParseError(message: String)

    Failed to parse data (CBOR, JSON, or authenticator data).

  • InvalidSignature

    The cryptographic signature verification failed.

  • CredentialNotAllowed

    The credential ID is not in the allowed credentials list or does not match the stored credential.

  • SignCountRegression

    The sign count did not strictly increase from a nonzero stored count, indicating a possible cloned authenticator.

  • UserPresenceFailed

    User presence was required but not asserted by the authenticator.

  • UserVerificationFailed

    User verification was required but not performed by the authenticator.

A parsed authentication response. Construct via response_decoder (when the response arrives nested in a larger JSON envelope) or parse_response_json (when you have a raw response string). Pass to verify or response_info.

pub opaque type Response

Parsed credential lookup info from an authentication response. Use in the discoverable (usernameless) flow to find the stored credential before calling verify.

pub type ResponseInfo {
  ResponseInfo(
    credential_id: BitArray,
    user_handle: option.Option(BitArray),
  )
}

Constructors

  • ResponseInfo(
      credential_id: BitArray,
      user_handle: option.Option(BitArray),
    )

    Arguments

    credential_id

    The credential ID the authenticator asserted. Look this up against your stored credentials to find the matching record.

    user_handle

    The user handle the authenticator returned: the same opaque bytes originally registered as User.id. None when the authenticator did not return one. Required for discoverable credentials, optional otherwise.

The account context for an authentication ceremony.

pub type User {
  AlreadyIdentifiedUser(user_handle: BitArray)
  DiscoveredUser(user_handle: BitArray)
}

Constructors

  • AlreadyIdentifiedUser(user_handle: BitArray)

    The account was identified before the ceremony began. The response may omit userHandle; when present, it must match this stored handle.

  • DiscoveredUser(user_handle: BitArray)

    The account was identified from a discoverable credential response. The response must contain a userHandle matching this stored handle.

Values

pub fn allow_credential(
  builder: Builder,
  id id: BitArray,
  transports transports: List(glasslock.Transport),
) -> Builder

Add a credential to the allow_credentials list. Pass the stored credential’s id and transports so the browser can route the request. With no calls the request is a discoverable (passkey) flow where the authenticator selects a credential.

pub fn allow_cross_origin(
  builder: Builder,
  allow: Bool,
) -> Builder

Allow cross-origin requests. Defaults to disallowed.

pub fn allowed_top_origin(
  builder: Builder,
  origin: String,
) -> Builder

Add a top-level origin to the cross-origin iframe allowlist. The allowlist is consulted only when the browser supplies a topOrigin field; older browsers omit the field even for cross-origin requests, in which case top-origin verification is skipped.

pub fn build(builder: Builder) -> #(json.Json, Challenge)

Generate authentication options and a challenge verifier from a builder.

The first element is a PublicKeyCredentialRequestOptionsJSON value ready to serialize or embed inside a response envelope. The second is the verifier to pass to verify.

pub fn encode_challenge(challenge: Challenge) -> String

Serialize an authentication challenge for out-of-process storage between the build and verify steps (signed cookie, database, etc.). Pair with parse_challenge to rehydrate.

Security

The returned string is not authenticated. If an attacker can tamper with the stored blob they can redirect verification by forging rp_id or origins. Store it somewhere the caller controls (server-side session, a signed cookie, etc.).

pub fn new(
  relying_party_id relying_party_id: String,
  origin origin: String,
) -> Builder

Start a new authentication request builder with the required fields.

Defaults: 1-minute timeout, no allowed credentials (discoverable/passkey flow), cross-origin disallowed. Layer on optional configuration with the setter functions before calling build.

relying_party_id must be a domain string such as "example.com", without a scheme, port, or path. The browser validates the domain syntax and whether this RP ID is allowed for the calling origin.

pub fn origin(builder: Builder, origin: String) -> Builder

Add an additional accepted origin. The origin passed to new is always included. The signed clientDataJSON.origin returned by the authenticator must match one of them exactly, so pass a serialized origin such as "https://example.com".

pub fn parse_challenge(
  encoded: String,
) -> Result(Challenge, Error)

Decode a previously-encoded authentication challenge.

pub fn parse_response_json(
  response_json: String,
) -> Result(Response, Error)

Parse a raw response JSON string into a Response.

pub fn response_decoder() -> decode.Decoder(Response)

Decoder for an authentication response. Use when the response arrives nested in a larger JSON envelope.

pub fn response_info(
  response: Response,
) -> Result(ResponseInfo, Error)

Extract the credential id and optional user handle for lookup (discoverable flow).

Once you have a parsed Response, call this to look up the stored credential before passing the same Response to verify.

pub fn timeout(
  builder: Builder,
  timeout: duration.Duration,
) -> Builder

Set the ceremony timeout. Defaults to 1 minute.

pub fn user_verification(
  builder: Builder,
  user_verification: glasslock.Verification,
) -> Builder

Set the user verification requirement. When unset the field is omitted from the JSON sent to the browser; the browser applies the spec default of preferred.

pub fn verify(
  response response: Response,
  challenge challenge: Challenge,
  stored stored: glasslock.Credential,
  user user: User,
) -> Result(glasslock.Credential, Error)

Verify a challenge response from the browser.

Takes a parsed Response (from response_decoder or parse_response_json), the challenge from build, the stored credential, and the stored handle of the account being authenticated.

For a discoverable flow, call response_info first, resolve the account and credential from storage, then pass DiscoveredUser(account.user_handle). Use AlreadyIdentifiedUser when the account was known before the ceremony began.

Returns an updated credential with the new sign count on success.

pub fn verify_json(
  response_json response_json: String,
  challenge challenge: Challenge,
  stored stored: glasslock.Credential,
  user user: User,
) -> Result(glasslock.Credential, Error)

Convenience wrapper around verify for callers whose response arrives as a raw JSON string: parses with parse_response_json then verifies.

Search Document