{"id":12634,"date":"2026-07-17T20:07:53","date_gmt":"2026-07-18T01:07:53","guid":{"rendered":"https:\/\/www.rushworth.us\/lisa\/?p=12634"},"modified":"2026-10-30T12:08:15","modified_gmt":"2026-10-30T17:08:15","slug":"oauth-token-exchange-verified-identity-is-not-unlimited-permission","status":"publish","type":"post","link":"https:\/\/www.rushworth.us\/lisa\/?p=12634","title":{"rendered":"OAuth Token Exchange: Verified Identity Is Not Unlimited Permission"},"content":{"rendered":"<p>An agent presents two credentials: one representing me and one identifying Agent XYZ. A token service returns an access token stating that Agent XYZ is acting on my behalf.<\/p>\n<p>What prevents the agent from taking that token somewhere I never authorized?<\/p>\n<p><strong>Not the actor claim alone.<\/strong> Secure delegation requires verified input credentials, controlled issuance, restricted output tokens, and enforcement by the receiving application. OAuth token exchange supplies the protocol; it does not automatically determine what I intended.<\/p>\n<p><strong>The RFC behind token exchange<\/strong><\/p>\n<p><strong>RFC 8693, OAuth 2.0 Token Exchange<\/strong>, defines an OAuth-based Security Token Service, or STS. A client submits existing security tokens and requests a new token suitable for a particular purpose. The exchange can change token format, target audience, or authorization context, including representing delegation or impersonation.<\/p>\n<p>The distinction matters:<\/p>\n<ul>\n<li><strong>Impersonation:<\/strong> the recipient sees the subject\u2019s identity without necessarily distinguishing the intermediary.<\/li>\n<li><strong>Delegation:<\/strong> the token preserves the subject and identifies the party acting for that subject.<\/li>\n<\/ul>\n<p>For our agent scenario, delegation is the useful model: Lisa remains the authorization subject, while Agent XYZ is recorded as the actor.<\/p>\n<p><strong>How an exchange works<\/strong><\/p>\n<p>An illustrative request to an STS token endpoint contains:<\/p>\n<p>grant_type=urn:ietf:params:oauth:grant-type:token-exchange<\/p>\n<p>subject_token=&lt;Lisa&#8217;s eligible access token&gt;<\/p>\n<p>subject_token_type=urn:ietf:params:oauth:token-type:access_token<\/p>\n<p>actor_token=&lt;Agent XYZ&#8217;s supported identity token&gt;<\/p>\n<p>actor_token_type=urn:ietf:params:oauth:token-type:jwt<\/p>\n<p>requested_token_type=urn:ietf:params:oauth:token-type:access_token<\/p>\n<p>audience=case-management-api<\/p>\n<p>scope=cases.read cases.update<\/p>\n<p>&nbsp;<\/p>\n<p>The subject_token provides the subject\u2019s security context; the optional actor_token identifies the proposed actor. audience, resource, and scope describe requested targets and permissions. These are <strong>requests<\/strong>, not issuer obligations. Client authentication is a separate consideration from submitting either token.<\/p>\n<p>The conceptual flow is:<\/p>\n<p>Existing credentials + requested target and permissions<\/p>\n<p>|<\/p>\n<p>v<\/p>\n<p>Security Token Service<\/p>\n<p>\u251c\u2500 Validate credentials<\/p>\n<p>\u251c\u2500 Evaluate exchange policy<\/p>\n<p>\u251c\u2500 Establish delegated authority<\/p>\n<p>\u2514\u2500 Issue a constrained token<\/p>\n<p>|<\/p>\n<p>v<\/p>\n<p>Resource API<\/p>\n<p>\u251c\u2500 Validate issued token<\/p>\n<p>\u2514\u2500 Authorize this operation<\/p>\n<p>&nbsp;<\/p>\n<p>An exchange bridges security contexts; it is not simply a JWT re-signing operation. RFC 8693 explicitly limits its scope to the exchange protocol rather than defining every deployment\u2019s trust and authorization policy.<\/p>\n<p><strong>The exchanger must verify\u2014not believe\u2014the inputs<\/strong><\/p>\n<p>The STS should not accept:<\/p>\n<p>subject = Lisa<\/p>\n<p>actor = Agent XYZ<\/p>\n<p>trust_me = true<\/p>\n<p>&nbsp;<\/p>\n<p>It must establish those identities from credentials validated under an explicitly configured trust relationship. Token-validation rules depend on the accepted token profile; the API must also validate the resulting token independently. For JWT access tokens using RFC 9068, validation includes the expected token type, issuer, audience, signature, and expiration. (<a href=\"https:\/\/datatracker.ietf.org\/doc\/rfc9068\/\">datatracker.ietf.org<\/a>)<\/p>\n<p>For this deployment, I would require the following issuance controls:<\/p>\n<table width=\"100%\">\n<thead>\n<tr>\n<td><strong>Boundary<\/strong><\/td>\n<td><strong>Required decision<\/strong><\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Input trust<\/td>\n<td>Is this issuer permitted to supply this kind of identity evidence?<\/td>\n<\/tr>\n<tr>\n<td>Credential validity<\/td>\n<td>Is the token authentic, current, and acceptable in this exchange context?<\/td>\n<\/tr>\n<tr>\n<td>Actor identity<\/td>\n<td>Which actor does the verified credential actually identify?<\/td>\n<\/tr>\n<tr>\n<td>Exchange eligibility<\/td>\n<td>May this client or actor exchange this subject\u2019s credential?<\/td>\n<\/tr>\n<tr>\n<td>Target authorization<\/td>\n<td>May this delegation reach the requested API?<\/td>\n<\/tr>\n<tr>\n<td>Permission authorization<\/td>\n<td>Which requested operations may the output token permit?<\/td>\n<\/tr>\n<tr>\n<td>Output binding<\/td>\n<td>Must the resulting token be bound to a particular presenting key?<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>&nbsp;<\/p>\n<p>These are recommended policy requirements\u2014not defaults guaranteed by enabling RFC 8693. Its may_act claim can express actor eligibility, but identifying an eligible actor is still different from authorizing every requested target or operation.<\/p>\n<p>Crucially, the resulting token is normally issued <strong>for the resource server to validate<\/strong>, not merely for the exchanger to recognize later. A resource server that trusts the STS must still enforce the token\u2019s intended use.<\/p>\n<p><strong>A signed actor claim is attribution, not consent<\/strong><\/p>\n<p>An illustrative delegated token might contain:<\/p>\n<p>{<\/p>\n<p>&#8220;iss&#8221;: &#8220;https:\/\/identity.example.org&#8221;,<\/p>\n<p>&#8220;sub&#8221;: &#8220;user:lisa&#8221;,<\/p>\n<p>&#8220;aud&#8221;: &#8220;case-management-api&#8221;,<\/p>\n<p>&#8220;scope&#8221;: &#8220;cases.read cases.update&#8221;,<\/p>\n<p>&#8220;act&#8221;: {<\/p>\n<p>&#8220;sub&#8221;: &#8220;spiffe:\/\/agents.example.org\/agent\/xyz&#8221;<\/p>\n<p>}<\/p>\n<p>}<\/p>\n<p>&nbsp;<\/p>\n<p>The issuer asserts that Lisa is the subject and Agent XYZ is the current actor. The signature protects that assertion against modification. The actor claim does <strong>not<\/strong>, by itself, establish that Lisa approved a specific case update or requested access to another application.<\/p>\n<p>This is the central distinction:<\/p>\n<p><strong>Who is acting, whose authority applies, and which action was authorized are separate facts.<\/strong><\/p>\n<p>If the deployment intentionally gives Agent XYZ all of Lisa\u2019s application rights, then an unexpected operation within those rights may be policy-authorized even though Lisa did not intend it. No token format can recover a narrower instruction that the authorization system never captured.<\/p>\n<p><strong>Preventing the \u201cLisa never asked for that\u201d failure<\/strong><\/p>\n<p>There are several different failures hiding inside that example. They require different controls.<\/p>\n<ol>\n<li><strong> Using the token at another application<\/strong><\/li>\n<\/ol>\n<p>Suppose Agent XYZ receives a token for the case-management API, then presents it to payroll.<\/p>\n<p>Payroll must reject it because payroll is not its intended audience. Sharing an issuer or signing key does not make a token valid for every application. RFC 9700 recommends audience-restricted access tokens to limit this kind of reuse.<\/p>\n<p>For this architecture, prefer one narrowly defined target per token and exact audience validation at each API.<\/p>\n<ol start=\"2\">\n<li><strong> Exchanging it again for an unauthorized application<\/strong><\/li>\n<\/ol>\n<p>Audience validation does not help if the agent can ask an STS to convert its case-management token into a payroll token.<\/p>\n<p>The recommended issuance policy is to authorize each permitted exchange edge explicitly:<\/p>\n<p>Lisa delegation + Agent XYZ \u2192 case-management: allowed<\/p>\n<p>Lisa delegation + Agent XYZ \u2192 payroll:\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 denied<\/p>\n<p>&nbsp;<\/p>\n<p>Changing audience is a legitimate purpose of exchange, so the defense is not \u201cnever change audience.\u201d It is <strong>never let possession of an input token alone authorize an arbitrary audience transition<\/strong>. This follows from the protocol\u2019s separation between the requested target and issuer policy.<\/p>\n<ol start=\"3\">\n<li><strong> Performing an unintended action within the correct API<\/strong><\/li>\n<\/ol>\n<p>Audience restriction cannot distinguish \u201cupdate case 4821\u201d from \u201cupdate every case Lisa can access.\u201d A broad cases.update scope may be equally insufficient.<\/p>\n<p><strong>RFC 9396, Rich Authorization Requests<\/strong>, defines authorization_details for expressing finer-grained authorization requirements using structured JSON. The application-specific authorization type must define what those details mean and how they are enforced.<\/p>\n<p>For example, a proposed\u2014not standardized\u2014authorization object could be:<\/p>\n<p>{<\/p>\n<p>&#8220;type&#8221;: &#8220;case_operation&#8221;,<\/p>\n<p>&#8220;actions&#8221;: [&#8220;update&#8221;],<\/p>\n<p>&#8220;case_ids&#8221;: [&#8220;4821&#8221;],<\/p>\n<p>&#8220;allowed_fields&#8221;: [&#8220;status&#8221;],<\/p>\n<p>&#8220;allowed_status_values&#8221;: [&#8220;resolved&#8221;]<\/p>\n<p>}<\/p>\n<p>&nbsp;<\/p>\n<p>In this design, Lisa approves that operation through a trusted authorization interaction. The STS issues authority no broader than the approved operation, and the API checks the actual resource and requested mutation against it.<\/p>\n<p>RFC 9396 does <strong>not<\/strong> automatically add this object to RFC 8693 exchanges. Supporting it requires an agreed issuer\/API profile and enforcement implementation. Structured authorization is useful only when the resource server understands and checks it.<\/p>\n<ol start=\"4\">\n<li><strong> Stealing and replaying the token<\/strong><\/li>\n<\/ol>\n<p>A bearer token can be used by whoever possesses it. A signed act claim can therefore attribute a stolen token to Agent XYZ even when another party presents it.<\/p>\n<p>Sender-constrained tokens address that problem by requiring proof of possession of an associated key. RFC 9700 recommends these alongside audience restrictions.<\/p>\n<p><strong>RFC 9449, DPoP<\/strong>, provides application-layer sender constraint. The client supplies signed proofs, and the recipient validates the proof and its relationship to the access token.<\/p>\n<p>DPoP proofs include request method and URI information, token binding through ath for protected-resource requests, and freshness\/replay controls. But DPoP does <strong>not<\/strong> ordinarily bind the business request body or establish Lisa\u2019s approval of its contents.<\/p>\n<p>A legitimate\u2014or compromised\u2014agent holding the bound key can still generate proofs for operations its token permits. Sender constraint prevents a different presenter from simply replaying the token; it does not make the authorized presenter trustworthy.<\/p>\n<p><strong>A practical task-bound delegation design<\/strong><\/p>\n<p>For the Lisa\/Agent XYZ scenario, I would implement the following:<\/p>\n<ol>\n<li><strong>Capture approval outside the agent\u2019s assertions.<\/strong> Store an approved delegation identifying Lisa, Agent XYZ, target application, operations, resources, and expiry.<\/li>\n<li><strong>Require that approval during exchange.<\/strong> Validate both credentials and check the requested output against the stored grant.<\/li>\n<li><strong>Issue narrowly.<\/strong> Restrict audience, operations, object identifiers, lifetime, and presenting key where supported.<\/li>\n<li><strong>Enforce at the API.<\/strong> Validate the token, then compare the actual operation and affected object with the grant.<\/li>\n<li><strong>Control further exchange.<\/strong> Deny unapproved target transitions and privilege expansion.<\/li>\n<li><strong>Record execution.<\/strong> Log subject, actor, delegation identifier, target, resource, authorization decision, and outcome.<\/li>\n<li><strong>Use state where necessary.<\/strong> For one-time approval, atomically consume the authorization at the operation boundary; a unique token identifier alone does not make a token single-use.<\/li>\n<\/ol>\n<p>This is a proposed deployment architecture combining token exchange, constrained access tokens, and fine-grained authorization\u2014not a claim that RFC 8693 implements these controls automatically.<\/p>\n<p><strong>The takeaway<\/strong><\/p>\n<p>Token exchange can turn verified credentials into a token trusted by another system. <strong>It must not turn identity evidence into unrestricted authority.<\/strong><\/p>\n<p>For the threat in question, insist on four distinct properties:<\/p>\n<ul>\n<li>Verified subject and actor.<\/li>\n<li>Authorized issuance for the requested target.<\/li>\n<li>Permissions bounded by the approved delegation.<\/li>\n<li>Resource-server enforcement of those bounds.<\/li>\n<\/ul>\n<p>\u201cAgent XYZ acting for Lisa\u201d explains attribution. <strong>\u201cAgent XYZ may perform this operation on this resource for Lisa\u201d is the authorization boundary.<\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>An agent presents two credentials: one representing me and one identifying Agent XYZ. A token service returns an access token stating that Agent XYZ is acting on my behalf. What prevents the agent from taking that token somewhere I never authorized? Not the actor claim alone. Secure delegation requires verified input credentials, controlled issuance, restricted &hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2243],"tags":[2246,1488,2244,2216],"class_list":["post-12634","post","type-post","status-publish","format-standard","hentry","category-spiffe","tag-agent-identity","tag-oauth","tag-spiffe","tag-spire"],"_links":{"self":[{"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/posts\/12634","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=12634"}],"version-history":[{"count":1,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/posts\/12634\/revisions"}],"predecessor-version":[{"id":12635,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=\/wp\/v2\/posts\/12634\/revisions\/12635"}],"wp:attachment":[{"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=12634"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=12634"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.rushworth.us\/lisa\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=12634"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}