Lockspire.Protocol.TokenExchange.Internal.AccessTokenSigner (lockspire v1.6.1)

Copy Markdown

Shared access-token issuance for all Lockspire grant paths.

Owns the single RFC 9068 at+jwt signing site, the opaque-token delegate, and the one-place format decision (per-client override -> server default -> :jwt).

Format resolution

The effective access-token format is resolved in exactly one place (resolve_format/2):

  1. a per-client access_token_format of :jwt or :opaque wins;
  2. otherwise (nil) the server-wide ServerPolicy.access_token_format supplied through the explicit dependency bundle is used;
  3. otherwise it falls back to :jwt.

Audience derivation

The four grant paths (authorization-code, refresh, device-code, CIBA) emit a LIST aud: the requested resource(s) when present, otherwise [client_id]. The RFC 8693 token-exchange path keeps a BARE-STRING aud == client_id via issue_exchange/4, preserving the historical exchange wire shape.

Security

The signing alg/kid are taken ONLY from the active signing key — never from client-controlled input — and none is never emitted. On a missing or invalid key the error path logs inspect(reason) only: no key material reaches logs.

Summary

Types

result()

@type result() :: {:ok, String.t(), String.t()} | {:error, struct()}

token_for_issuance()

@type token_for_issuance() :: %Lockspire.Domain.Token{
  account_id: term(),
  audience: term(),
  client_id: term(),
  cnf: term(),
  code_challenge: term(),
  code_challenge_method: term(),
  consent_grant_id: term(),
  expires_at: term(),
  family_id: term(),
  generation: term(),
  id: term(),
  inserted_at: term(),
  interaction_id: term(),
  issued_at: term(),
  jti: term(),
  parent_token_id: term(),
  redeemed_at: term(),
  redirect_uri: term(),
  reuse_detected_at: term(),
  revoked_at: term(),
  scopes: term(),
  sid: term(),
  token_hash: String.t() | nil,
  token_type: term(),
  updated_at: term()
}

Functions

issue(token, client, request)

issue(token, client, request, dependencies)

Issue an access token for a standard grant path.

Resolves the effective format and returns {:ok, raw, hash} where hash == Lockspire.Security.Policy.hash_token(raw), or a 500 :token_signing_failed error when the :jwt branch cannot sign.

The :jwt branch emits a LIST aud derived from token.audience.

issue_exchange(token, client, custom_claims, request)

@spec issue_exchange(token_for_issuance(), Lockspire.Domain.Client.t(), map(), map()) ::
  result()

issue_exchange(token, client, custom_claims, request, dependencies)

@spec issue_exchange(
  token_for_issuance(),
  Lockspire.Domain.Client.t(),
  map(),
  map(),
  map()
) :: result()

Issue a signed at+jwt access token for the RFC 8693 token-exchange path.

Always :jwt. Emits a BARE-STRING aud == client.client_id (the exchange carve-out) and merges custom_claims over the base claims after dropping the restricted claims iss sub aud exp iat jti client_id.