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
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).
-
InvalidSignatureThe cryptographic signature verification failed.
-
CredentialNotAllowedThe credential ID is not in the allowed credentials list or does not match the stored credential.
-
SignCountRegressionThe sign count did not strictly increase from a nonzero stored count, indicating a possible cloned authenticator.
-
UserPresenceFailedUser presence was required but not asserted by the authenticator.
-
UserVerificationFailedUser 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.Nonewhen 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
userHandlematching 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.