You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
NyxID currently advertises RFC 8693
Token Exchange (delegated access) in docs/OIDC.md, docs/API.md, and docs/MCP_DELEGATION_FLOW.md.
What we actually ship is a purpose-built subset of the spec — three
concrete grant shapes covering self-delegation, OAuth-broker binding
exchange, and social-IdP token exchange. It is not a complete RFC 8693
server. This thread records what is and isn't implemented so future
readers don't mistake "supported in OIDC.md" for "spec-complete". If we
need fuller RFC 8693 semantics (real impersonation chains, audience-bound
tokens, multi-token-type conversion), the checklist at the end is the
backlog.
⚠️ Intentionally None for the access_token and broker branches (oauth.rs:1438,1552); set only on the social branch. RFC §2.2.1 allows omission, but downstream readers may expect it.
Standard OAuth error codes are emitted by errors/mod.rs
(invalid_request / invalid_client / invalid_grant / invalid_scope). Format conforms to RFC 6749 (oauth.rs:250-268).
We emit an act.sub = client_id on tokens minted by the
self-delegation branch (jwt.rs:149-153, the ActorClaim struct). We do not consume or chain act on input — see Gaps §A.1 below.
Sender-constrained tokens
DPoP and mTLS are validated only on the broker branch
(oauth.rs:1463-1511). The access_token → delegated branch does not
bind a confirmation key, so a leaked delegated token is bearer-only.
Gaps (vs the full RFC)
A. Impersonation / delegation modelling
actor_token is not accepted. RFC 8693 §1.3
defines two distinct semantics — impersonation (token requester gets
a token that hides the chain) and delegation (token has nested act claims that name every actor). Today we cannot accept "actor B
asks to act on behalf of subject A, here is B's own token in actor_token". Adding this is a prerequisite for any multi-hop
delegation chain across services.
act chain is single-level.jwt::generate_delegated_access_token
sets act.sub = client_id once. RFC 8693 §4.1
says nested act must accumulate when one delegated token is
exchanged for another — but we reject chained delegation outright
(token_exchange_service.rs:59-70) to prevent indefinite TTL extension.
This is correct given the absence of audience/scope narrowing; once
§B is in, the right answer is "allow chaining but force monotonic
narrowing", not blanket reject.
B. Audience / resource scoping
audience not implemented. Tokens issued today carry only aud=client_id (or NyxID's own audience), not RFC 8707 resource
indicators. A delegated token is therefore implicitly usable against
any NyxID-protected resource the scope permits — there is no
per-request audience narrowing.
resource not implemented. Same shape as audience. Should be
handled together.
C. Token-type coverage
Only access_token (and the private broker URI) accepted as subject_token_type. RFC 8693 §3
lists six standard URIs (access_token, refresh_token, id_token, saml1, saml2, jwt). We are not interoperable as a target for
clients that hold any other token type.
Only access_token issued. No requested_token_type honoring,
so we cannot mint id_token or jwt on demand.
Discovery does not advertise the broker URI. oidc_discovery.rs:30 lists only the standard access_token URI;
the private urn:nyxid:...:binding-id is not in subject_token_types_supported / issued_token_types_supported,
making the broker branch invisible to spec-driven clients.
D. Sender-constraint asymmetry
Self-delegation branch has no DPoP / mTLS binding. The broker
branch enforces sender-constraint (oauth.rs:1463-1511); the
higher-trust self-delegation branch does not. A leaked 5-minute
delegated bearer token is fully usable from anywhere. This is
tolerable for short TTL but a real RFC 8693 deployment with longer
TTLs would need confirmation-key binding here.
E. Client-side gaps that show up here
The aevatar client (NyxIdRemoteCapabilityBroker) does not parse issued_token_type from our response. We emit the field correctly,
but no current consumer reads it. Tracked in the aevatar companion
thread linked above.
F. Test coverage
token_exchange_service.rs:266-305 only unit-tests validate_delegation_scope and a placeholder for the chained-delegation
guard. There is no end-to-end test that posts to /oauth/token
with a urn:ietf:params:oauth:grant-type:token-exchange body and
asserts on the full response shape, error code mapping, or DPoP
binding round-trip.
Why we are where we are
The current shape was driven by three concrete callers:
OIDC-linked downstream services that need server-to-server calls on
behalf of a logged-in user
(MCP_DELEGATION_FLOW.md
Flow B).
aevatar's per-user NyxID broker binding model
(ADR-0018),
where aevatar must never hold a refresh token but needs to exchange an
opaque binding pointer for a 5-minute access token per turn.
For all three, actor_token and audience were out of scope; refresh
tokens are deliberately omitted on the delegated branches to bound TTL
extension. The implementation matches the use cases. It does not
match the full spec.
Future work — if and when we want a complete RFC 8693 server
Roughly ordered by ratio of payoff to cost:
Advertise honestly in discovery. Add subject_token_types_supported, issued_token_types_supported, and
the broker URI (or hide it) in /.well-known/openid-configuration. Cheap, prevents clients from
making wrong assumptions.
End-to-end tests on /oauth/token with the token-exchange grant.
Cover happy path + every error-code branch + DPoP round-trip + the
chained-delegation guard. Cheap, prevents silent regressions.
audience / resource parameters. Validate against
per-resource-server registry; bake aud into the issued token. This
is what unlocks a meaningful audit story ("this delegated token can
only call resource X").
actor_token + nested act chaining, paired with monotonic
scope/audience narrowing so the existing chained-delegation rejection
becomes redundant. Significant work — needs an ADR on how multi-hop
actor identity is modelled and how delegated flag interacts with a
non-empty act chain.
requested_token_type=jwt / id_token. Add a self-contained
JWT issuance path so token-exchange can hand out audience-bound,
self-describing tokens for non-NyxID-protected downstream services.
DPoP / mTLS confirmation on the self-delegation branch. Mirror
the broker branch's sender-constraint code.
Standardise the broker subject_token_type. Replace urn:nyxid:...:binding-id with a JWT-wrapped binding so clients can
use urn:ietf:params:oauth:token-type:jwt and remain spec-portable.
Requires changing the wire contract — coordinate with aevatar.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
TL;DR
NyxID currently advertises RFC 8693
Token Exchange (delegated access) in
docs/OIDC.md,docs/API.md, anddocs/MCP_DELEGATION_FLOW.md.What we actually ship is a purpose-built subset of the spec — three
concrete grant shapes covering self-delegation, OAuth-broker binding
exchange, and social-IdP token exchange. It is not a complete RFC 8693
server. This thread records what is and isn't implemented so future
readers don't mistake "supported in OIDC.md" for "spec-complete". If we
need fuller RFC 8693 semantics (real impersonation chains, audience-bound
tokens, multi-token-type conversion), the checklist at the end is the
backlog.
Companion thread on the aevatar (client) side:
aevatarAI/aevatar#527
Context
urn:ietf:params:oauth:grant-type:token-exchangegrant).POST /oauth/token(shared with all grants).backend/src/handlers/oauth.rs:1322-1562— grant dispatch and threesubject_token_typebranches (social /access_token/ brokerbinding-id)backend/src/services/token_exchange_service.rs— theaccess_token → delegated access_tokenbranch (5-minute TTL,consent-checked, scope-validated, chained-delegation rejected)
backend/src/services/oauth_broker_service.rs— broker bindingexchange branch
backend/src/services/social_token_exchange_service.rs— provider(Google / GitHub) token → NyxID token branch
backend/src/handlers/oidc_discovery.rs:30,51— discovery metadataWhat is actually implemented
Branches recognised at the token endpoint
subject_token_typeurn:ietf:params:oauth:token-type:access_token(noprovider)token_exchange_service::exchange_tokenact.sub = client_id. Rejects chained delegation (token_exchange_service.rs:59-70).urn:ietf:params:oauth:token-type:access_token+providersetsocial_token_exchange_service::exchange_social_tokenSOCIAL_TOKEN_EXCHANGE_MOBILE_INTEGRATION.md).urn:nyxid:...:token-type:binding-id(private URI)oauth_broker_service::exchange_via_bindingoauth.rs:1463-1511).BadRequest("Unsupported subject_token_type: ...")RFC 8693 §2.1 request parameters
grant_typeoauth.rs:1322subject_tokenoauth.rs:1346-1349subject_token_typeoauth.rs:1350-1353scopetoken_exchange_service.rs:85-88validates against client'sdelegation_scopesactor_tokenactor_token_typerequested_token_typeurn:ietf:params:oauth:token-type:access_tokenaudienceresourceRFC 8693 §2.2.1 response
access_tokenissued_token_typeoauth.rs:1442,1556,1385)token_typeBearerexpires_inscoperefresh_tokenNonefor theaccess_tokenand broker branches (oauth.rs:1438,1552); set only on the social branch. RFC §2.2.1 allows omission, but downstream readers may expect it.Error handling (RFC 8693 §2.2.2 / RFC 6749 §5.2)
Standard OAuth error codes are emitted by
errors/mod.rs(
invalid_request/invalid_client/invalid_grant/invalid_scope). Format conforms toRFC 6749 (
oauth.rs:250-268).actclaim (RFC 8693 §4.1)We emit an
act.sub = client_idon tokens minted by theself-delegation branch (
jwt.rs:149-153, theActorClaimstruct). We donot consume or chain
acton input — see Gaps §A.1 below.Sender-constrained tokens
DPoP and mTLS are validated only on the broker branch
(
oauth.rs:1463-1511). Theaccess_token → delegatedbranch does notbind a confirmation key, so a leaked delegated token is bearer-only.
Gaps (vs the full RFC)
A. Impersonation / delegation modelling
actor_tokenis not accepted.RFC 8693 §1.3
defines two distinct semantics — impersonation (token requester gets
a token that hides the chain) and delegation (token has nested
actclaims that name every actor). Today we cannot accept "actor Basks to act on behalf of subject A, here is B's own token in
actor_token". Adding this is a prerequisite for any multi-hopdelegation chain across services.
actchain is single-level.jwt::generate_delegated_access_tokensets
act.sub = client_idonce.RFC 8693 §4.1
says nested
actmust accumulate when one delegated token isexchanged for another — but we reject chained delegation outright
(
token_exchange_service.rs:59-70) to prevent indefinite TTL extension.This is correct given the absence of audience/scope narrowing; once
§B is in, the right answer is "allow chaining but force monotonic
narrowing", not blanket reject.
B. Audience / resource scoping
audiencenot implemented. Tokens issued today carry onlyaud=client_id(or NyxID's own audience), notRFC 8707 resource
indicators. A delegated token is therefore implicitly usable against
any NyxID-protected resource the scope permits — there is no
per-request audience narrowing.
resourcenot implemented. Same shape asaudience. Should behandled together.
C. Token-type coverage
access_token(and the private broker URI) accepted assubject_token_type.RFC 8693 §3
lists six standard URIs (
access_token,refresh_token,id_token,saml1,saml2,jwt). We are not interoperable as a target forclients that hold any other token type.
access_tokenissued. Norequested_token_typehonoring,so we cannot mint
id_tokenorjwton demand.oidc_discovery.rs:30lists only the standardaccess_tokenURI;the private
urn:nyxid:...:binding-idis not insubject_token_types_supported/issued_token_types_supported,making the broker branch invisible to spec-driven clients.
D. Sender-constraint asymmetry
branch enforces sender-constraint (
oauth.rs:1463-1511); thehigher-trust self-delegation branch does not. A leaked 5-minute
delegated bearer token is fully usable from anywhere. This is
tolerable for short TTL but a real RFC 8693 deployment with longer
TTLs would need confirmation-key binding here.
E. Client-side gaps that show up here
NyxIdRemoteCapabilityBroker) does not parseissued_token_typefrom our response. We emit the field correctly,but no current consumer reads it. Tracked in the aevatar companion
thread linked above.
F. Test coverage
token_exchange_service.rs:266-305only unit-testsvalidate_delegation_scopeand a placeholder for the chained-delegationguard. There is no end-to-end test that posts to
/oauth/tokenwith a
urn:ietf:params:oauth:grant-type:token-exchangebody andasserts on the full response shape, error code mapping, or DPoP
binding round-trip.
Why we are where we are
The current shape was driven by three concrete callers:
behalf of a logged-in user
(
MCP_DELEGATION_FLOW.mdFlow B).
(ADR-0018),
where aevatar must never hold a refresh token but needs to exchange an
opaque binding pointer for a 5-minute access token per turn.
(
SOCIAL_TOKEN_EXCHANGE_MOBILE_INTEGRATION.md),exchanging Google / GitHub tokens for NyxID tokens.
For all three,
actor_tokenandaudiencewere out of scope; refreshtokens are deliberately omitted on the delegated branches to bound TTL
extension. The implementation matches the use cases. It does not
match the full spec.
Future work — if and when we want a complete RFC 8693 server
Roughly ordered by ratio of payoff to cost:
subject_token_types_supported,issued_token_types_supported, andthe broker URI (or hide it) in
/.well-known/openid-configuration. Cheap, prevents clients frommaking wrong assumptions.
/oauth/tokenwith the token-exchange grant.Cover happy path + every error-code branch + DPoP round-trip + the
chained-delegation guard. Cheap, prevents silent regressions.
audience/resourceparameters. Validate againstper-resource-server registry; bake
audinto the issued token. Thisis what unlocks a meaningful audit story ("this delegated token can
only call resource X").
actor_token+ nestedactchaining, paired with monotonicscope/audience narrowing so the existing chained-delegation rejection
becomes redundant. Significant work — needs an ADR on how multi-hop
actor identity is modelled and how
delegatedflag interacts with anon-empty
actchain.requested_token_type=jwt/id_token. Add a self-containedJWT issuance path so token-exchange can hand out audience-bound,
self-describing tokens for non-NyxID-protected downstream services.
the broker branch's sender-constraint code.
subject_token_type. Replaceurn:nyxid:...:binding-idwith a JWT-wrapped binding so clients canuse
urn:ietf:params:oauth:token-type:jwtand remain spec-portable.Requires changing the wire contract — coordinate with aevatar.
References
docs/OIDC.md— current spec-support table (line 33)docs/MCP_DELEGATION_FLOW.md— Flow B usagedocs/SOCIAL_TOKEN_EXCHANGE_MOBILE_INTEGRATION.md— social branchdocs/ARCHITECTURE.md— references at:214,:1493,:1583Beta Was this translation helpful? Give feedback.
All reactions