AttestoPhoenix.PrincipalStore behaviour (AttestoPhoenix v4.2.1)

Copy Markdown View Source

The host-owned subject/principal contract.

The library resolves the subject during protected-resource authentication and builds the principal map minted into issued tokens, but the subject source (the host's user store) and the claim shaping are host policy. A host implements this behaviour and wires each callback into AttestoPhoenix.Config; this module is the contract those keys install and the recommended production shape.

Each @callback corresponds to the identically named AttestoPhoenix.Config key:

  • load_principal/1 (:load_principal, required)
  • principal_kinds/0 (:principal_kinds)
  • build_principal/3 (:build_principal)
  • build_refresh_principal/2 (:build_refresh_principal)
  • resolve_jwt_bearer_subject/1 (:resolve_jwt_bearer_subject, required only when the ID-JAG jwt-bearer grant is enabled)

Summary

Types

The host's opaque principal/subject representation.

Security context for a validated and atomically rotated refresh grant.

Callbacks

Build the principal map passed to Attesto.Token.mint/3 for an authorization-code or client_credentials grant. Receives the resolved client, the subject identifier, and the granted scope. The returned map carries at least :sub and any host-owned claims.

Revalidate host-owned principal and session state for a refresh grant, and build the principal map used without a second build_principal/3 lookup. Protocol-owned claims and the authenticated client_id are reconciled before the map is passed to Attesto.Token.mint/3.

Resolve the subject/principal by its identifier during protected-resource authentication. Returns {:ok, principal} or {:error, :not_found}.

Return the non-empty principal-kind catalog passed to Attesto.Config.

Map a validated Identity Assertion JWT Authorization Grant (ID-JAG) to a local subject for the urn:ietf:params:oauth:grant-type:jwt-bearer grant (draft-ietf-oauth-identity-assertion-authz-grant-04).

Types

principal()

@type principal() :: term()

The host's opaque principal/subject representation.

refresh_context()

@type refresh_context() :: %{
  subject: String.t(),
  client_id: String.t(),
  issuer: String.t(),
  scope: [String.t()],
  resource: [String.t()],
  family_id: String.t(),
  generation: non_neg_integer(),
  acr: String.t() | nil,
  auth_time: non_neg_integer() | nil,
  session_id: String.t() | nil
}

Security context for a validated and atomically rotated refresh grant.

:scope is the effective scope after token-endpoint policy. :resource, :subject, :issuer, authentication context, family identity, and :session_id come from the persisted grant rather than request parameters. :client_id is the client identity authenticated for this token request.

Callbacks

build_principal(client, subject, scope)

(optional)
@callback build_principal(client :: term(), subject :: String.t(), scope :: [String.t()]) ::
  map()

Build the principal map passed to Attesto.Token.mint/3 for an authorization-code or client_credentials grant. Receives the resolved client, the subject identifier, and the granted scope. The returned map carries at least :sub and any host-owned claims.

The returned :sub MUST be namespaced with the matching Attesto.PrincipalKind sub_prefix - Attesto.Token rejects an unprefixed subject at mint time (:invalid_sub). For the client_credentials grant (RFC 6749 §4.4) the subject handed in is the OAuth client_id, and Dynamic Client Registration (RFC 7591 §3.2.1) issues that id unprefixed: this callback is the sole place the kind prefix is applied. The prefix is mint-time defense-in-depth (a token's sub stays unambiguous across principal kinds), so namespacing here is mandatory rather than cosmetic.

build_refresh_principal(client, context)

(optional)
@callback build_refresh_principal(client :: term(), context :: refresh_context()) ::
  map() | {:error, :invalid_grant}

Revalidate host-owned principal and session state for a refresh grant, and build the principal map used without a second build_principal/3 lookup. Protocol-owned claims and the authenticated client_id are reconciled before the map is passed to Attesto.Token.mint/3.

This callback runs after the refresh credential, authenticated client, sender constraint, scope, resource, and rotation state have been validated atomically, but before an access token is minted or the successor refresh token is returned. Return a principal map to allow issuance. Return {:error, :invalid_grant} when the subject is locked or deactivated, its session is terminated, or its persisted tenant context is no longer valid. A denial revokes the entire refresh family and produces a generic OAuth invalid_grant response.

The callback receives only stable grant identifiers and authorization context; it never receives either plaintext refresh token. It is optional so existing installations retain their current build_principal/3 behavior. Immediate lost-response retries can invoke it again for the same family and generation, so keep it side-effect-free or idempotent.

load_principal(subject_id)

@callback load_principal(subject_id :: String.t()) ::
  {:ok, principal()} | {:error, :not_found}

Resolve the subject/principal by its identifier during protected-resource authentication. Returns {:ok, principal} or {:error, :not_found}.

principal_kinds()

(optional)
@callback principal_kinds() :: [Attesto.PrincipalKind.t(), ...]

Return the non-empty principal-kind catalog passed to Attesto.Config.

Each kind gives a token subject class its claim value and unambiguous sub prefix. This is host identity policy; the library cannot infer it from an Ecto schema or client record.

resolve_jwt_bearer_subject(claims)

(optional)
@callback resolve_jwt_bearer_subject(claims :: map()) ::
  {:ok, subject :: String.t()} | String.t() | {:error, term()}

Map a validated Identity Assertion JWT Authorization Grant (ID-JAG) to a local subject for the urn:ietf:params:oauth:grant-type:jwt-bearer grant (draft-ietf-oauth-identity-assertion-authz-grant-04).

Receives the string-keyed, already-verified assertion claims after sender binding, resource, scope, and host policy have passed. The signature, trusted iss, aud, client_id binding, and exp/iat have all been checked; the atomic jti replay claim follows this callback because the replay-store seam has no non-consuming reservation operation. A validly signed replay can therefore reach this callback before rejection, so callbacks with side effects must be idempotent. The host maps the asserted external identity - typically claims["sub"] (unique when scoped with claims["iss"]) and/or claims["email"] - to the local subject the issued token is minted for, the same subject string build_principal/3 then receives.

Returns {:ok, subject} (or a bare subject string) to authorize, or {:error, reason} (or any non-subject value) to deny - a deny becomes RFC 6749 §5.2 invalid_grant. Required only when the jwt-bearer grant is enabled (AttestoPhoenix.Config enforces this at boot).