The resource server's half of the Identity Assertion JWT Authorization Grant
(ID-JAG), the grant behind MCP Enterprise-Managed Authorization (EMA) -
draft-ietf-oauth-identity-assertion-authz-grant-04.
A token request arrives with
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer and an assertion
parameter carrying an ID-JAG: a short-lived JWT the enterprise IdP signed
(after its own RFC 8693 token exchange) asserting one user for this resource
application. authorize/3 turns that assertion into the local subject and
scope ceiling the token endpoint mints from. It owns the stateful concerns
that Attesto.IdentityAssertion (conn-free, pure) deliberately leaves out:
- issuer trust - the assertion's
issmust be a configured trusted issuer (jwt_bearer: [issuers: %{...}]); an unconfigured issuer is denied without revealing which issuers are trusted. - JWKS resolution - static keys, a cached
jwks_urifetch (reusing the SSRF-guarded CIMD fetcher + cache), or a custom:jwks_resolver. jtireplay - prepares an issuer-scoped, fixed-size identity for the configured:replay_checkseam; the token core claims it only after all exchange policy has passed, and before minting.- subject resolution - after request policy passes, the host's
:resolve_jwt_bearer_subjectcallback maps the validated claims to a local principal subject (or denies).
Every failure returns {:error, atom}; the token core
(AttestoPhoenix.AuthorizationServer.Token) maps a missing assertion
parameter to RFC 6749 §5.2 invalid_request and every assertion/trust/replay/
subject failure to invalid_grant, as the draft requires.
This is NOT private_key_jwt client authentication (RFC 7523 §3) nor the
RFC 8693 token-exchange grant (which runs at the IdP).
Summary
Types
A verified assertion's fixed-size replay identity and remaining acceptance window.
The resolved local subject, the assertion's scope ceiling (nil when the
assertion carried no scope claim, so the host policy alone decides), and the
validated claims.
Functions
Validate the ID-JAG assertion, claim its replay identity, and resolve the
local subject.
Atomically claim a replay identity returned by prepare/3 before minting.
Validate an ID-JAG while deferring subject resolution and its atomic replay claim.
Resolve verified ID-JAG claims to a local principal subject.
Types
@type error() ::
:missing_assertion
| :untrusted_issuer
| :jwks_unavailable
| :invalid_assertion
| :replay
| :subject_denied
@type pending_claim() :: {String.t(), pos_integer()}
A verified assertion's fixed-size replay identity and remaining acceptance window.
@type prepared_result() :: %{ scope_ceiling: [String.t()] | nil, claims: Attesto.IdentityAssertion.claims(), replay_claim: pending_claim() }
@type result() :: %{ subject: String.t(), scope_ceiling: [String.t()] | nil, claims: Attesto.IdentityAssertion.claims() }
The resolved local subject, the assertion's scope ceiling (nil when the
assertion carried no scope claim, so the host policy alone decides), and the
validated claims.
Functions
@spec authorize(AttestoPhoenix.Config.t(), String.t() | nil, map()) :: {:ok, result()} | {:error, error()}
Validate the ID-JAG assertion, claim its replay identity, and resolve the
local subject.
client_id is the already-authenticated client's identifier (the token
endpoint resolved it from client authentication); the assertion's client_id
claim MUST equal it. Returns {:ok, %{subject, scope_ceiling, claims}} or
{:error, t:error/0}.
@spec commit_replay_claim(AttestoPhoenix.Config.t(), pending_claim()) :: :ok | {:error, :replay}
Atomically claim a replay identity returned by prepare/3 before minting.
@spec prepare(AttestoPhoenix.Config.t(), String.t() | nil, map()) :: {:ok, prepared_result()} | {:error, error()}
Validate an ID-JAG while deferring subject resolution and its atomic replay claim.
The token core uses this variant so sender binding, resource, scope, and host
policy can finish before invoking a potentially side-effecting subject
resolver or consuming the assertion. A caller MUST then call
resolve_subject/2 and pass :replay_claim to commit_replay_claim/2 before
minting or releasing an authorization result. Most callers should use
authorize/3, which performs both steps itself.
@spec resolve_subject(AttestoPhoenix.Config.t(), Attesto.IdentityAssertion.claims()) :: {:ok, String.t()} | {:error, :subject_denied}
Resolve verified ID-JAG claims to a local principal subject.
Required when the feature is enabled (Config.validate!/1 enforces this), so
an unset callback is a configuration fault rather than a per-request denial.