WaxAPIREST.Callback behaviour (wax_api_rest v0.5.0)

View Source

Behaviour for the callback module that implements various tasks for WaxAPIREST.Plug

Note that users shall be authenticated in some manner when registering - relying on the provided username would be insecure as it would allow an attacker to register new key (and then authenticate) for any existing account.

Summary

Types

Key data

User information

List of registered user keys

Callbacks

Returns the current challenge

Invalidates the current challenge to prevent replay attacks

Callback called on Plug.Conn.t/0 upon successful authentication

Save the current attestation or authentication challenge and returns the connection

Returns the user info required for WebAuthn

Returns the user keys

Types

key_data()

@type key_data() :: %{
  :cose_key => Wax.CoseKey.t(),
  optional(:transports) => [WaxAPIREST.Types.AuthenticatorTransport.t()] | nil,
  optional(:sign_count) => non_neg_integer()
}

Key data

user_info()

@type user_info() :: %{
  :id => String.t(),
  optional(:name) => String.t(),
  optional(:display_name) => String.t()
}

User information

The returned id field must be URL base64 encoded no longer than 64 bytes.

user_keys()

@type user_keys() ::
  [{credential_id :: Wax.CredentialId.t(), key_data()}]
  | %{optional(credential_id :: Wax.CredentialId.t()) => key_data()}

List of registered user keys

Callbacks

get_challenge(conn)

@callback get_challenge(conn :: Plug.Conn.t()) :: Wax.Challenge.t() | no_return()

Returns the current challenge

Security: Challenges MUST be expired after a reasonable timeout (recommended: 5 minutes). This callback should validate that the challenge has not expired before returning it. Expired challenges should raise an exception to prevent replay attacks.

If a fault occurs or the challenge has expired, an exception can be raised. Its error message will be displayed in the JSON error response.

invalidate_challenge(conn)

@callback invalidate_challenge(conn :: Plug.Conn.t()) :: Plug.Conn.t() | no_return()

Invalidates the current challenge to prevent replay attacks

This callback is called after successful authentication or registration to ensure that challenges cannot be reused. Challenges MUST be invalidated after use to prevent replay attacks.

If a fault occurs an exception can be raised. Its error message will be displayed in the JSON error response.

on_authentication_success(conn, credential_id, authenticator_data)

@callback on_authentication_success(
  conn :: Plug.Conn.t(),
  credential_id :: Wax.CredentialId.t(),
  authenticator_data :: Wax.AuthenticatorData.t()
) :: Plug.Conn.t() | no_return()

Callback called on Plug.Conn.t/0 upon successful authentication

Can be used to set a value (cookie...) in the connection.

Signature counter update should be performed at this step, if supported.

If a fault occurs an exception can be raised. Its error message will be displayed in the JSON error response.

put_challenge(conn, challenge)

@callback put_challenge(
  conn :: Plug.Conn.t(),
  challenge :: Wax.Challenge.t()
) :: Plug.Conn.t() | no_return()

Save the current attestation or authentication challenge and returns the connection

Security: Challenges MUST be stored with a timestamp and expired after a reasonable timeout (recommended: 5 minutes). The get_challenge/1 callback should validate expiration before returning challenges.

If a fault occurs an exception can be raised. Its error message will be displayed in the JSON error response.

register_key(conn, credential_id, authenticator_data, attestation_result)

@callback register_key(
  conn :: Plug.Conn.t(),
  credential_id :: Wax.CredentialId.t(),
  authenticator_data :: Wax.AuthenticatorData.t(),
  attestation_result :: Wax.Attestation.result()
) :: Plug.Conn.t() | no_return()

Saves a new attestation key for a user

The COSE key can be retrieved in the authenticator data using:

authenticator_data.attested_credential_data.credential_public_key

The COSE key is a map of integers (both keys and values). This can be conveniently saved in Erlang / Elixir using Erlang's term_to_binary/1 function (and possibly Base.encode64/1 if the database doesn't accept binary values).

The signature count can also be checked against the value saved in the database:

authenticator_data.sign_count

A single user can register several keys. Each key is identified by its key_id.

It returns the connection. This can be used to set a value (cookie...) in it.

If a fault occurs an exception can be raised. Its error message will be displayed in the JSON error response.

user_info(conn)

@callback user_info(conn :: Plug.Conn.t()) :: user_info() | no_return()

Returns the user info required for WebAuthn

This callback is called during attestation and authentication option request and must return the identifier of the user. This identifier is the user handle of the user account entity. As stated by the specification:

A user handle is an opaque byte sequence with a maximum size of 64 bytes. User handles are not meant to be displayed to users. The user handle SHOULD NOT contain personally identifying information about the user, such as a username or e-mail address;

The callback shall, however, return a string and not a binary as it will not be converted to a string.

It can also returns the name and display name of the user (see WaxAPIREST.Callback.user_info/0). If not provided, it will be defaulted to the request's values.

If the user doesn't exist or a fault occurs when retrieving user information, an exception can be raised. Its error message will be displayed in the JSON error response.

user_keys(conn)

@callback user_keys(conn :: Plug.Conn.t()) :: user_keys()

Returns the user keys

If a fault occurs an exception can be raised. Its error message will be displayed in the JSON error response.