<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="rfc2629.xslt"?>
<!DOCTYPE rfc [
  <!ENTITY nbsp    "&#160;">
  <!ENTITY zwsp    "&#8203;">
  <!ENTITY nbhy    "&#8209;">
  <!ENTITY wj      "&#8288;">
]>
<rfc xmlns:xi="http://www.w3.org/2001/XInclude"
     ipr="trust200902"
     docName="draft-moros-oauth-browser-session-handoff-00"
     category="info"
     submissionType="independent"
     xml:lang="en"
     tocInclude="true"
     tocDepth="3"
     symRefs="true"
     sortRefs="true"
     version="3">

  <front>
    <title abbrev="OAuth Browser Session Handoff">
      Browser Session Establishment Using OAuth 2.0 Token Exchange and Short-Lived Authorization Codes
    </title>
    <seriesInfo name="Internet-Draft" value="draft-moros-oauth-browser-session-handoff-00"/>

    <author fullname="Gio Moros" initials="G." surname="Moros">
      <organization>Midas Labs</organization>
      <address>
        <postal>
          <country>CA</country>
        </postal>
        <email>Gio@midaslabs.ca</email>
      </address>
    </author>

    <date year="2026"/>

    <area>Security</area>
    <workgroup>Individual Submission</workgroup>

    <keyword>OAuth</keyword>
    <keyword>Token Exchange</keyword>
    <keyword>Authorization Code</keyword>
    <keyword>Session</keyword>
    <keyword>Browser</keyword>
    <keyword>JWT</keyword>

    <abstract>
      <t>
        This document specifies a usage profile that composes OAuth 2.0 Token
        Exchange <xref target="RFC8693"/> with a short-lived, single-use
        authorization code to establish an authenticated browser session at a
        Relying Party (RP) on behalf of a user authenticated at an Identity
        Provider (IdP) operating an independent OAuth 2.0 Security Token
        Service (STS). The server-to-server leg uses RFC 8693 to convey user
        identity and authorization context to the RP's STS, which issues an
        RP-scoped access token. A short-lived opaque code is then used to
        mediate delivery of session state into the user's browser without
        ever exposing the access token on the front channel.
      </t>
      <t>
        The profile is designed to avoid the class of front-channel token
        leakage (browser history, HTTP Referer header, intermediary access
        logs) that motivated the deprecation of the OAuth 2.0 implicit grant
        in <xref target="RFC9700"/>. The profile also defines a minimum
        claims contract on the RP-issued access token to enable stateless
        authorization enforcement at the RP without synchronous callbacks to
        the IdP.
      </t>
    </abstract>
  </front>

  <middle>

    <!-- =================================================== -->
    <section anchor="introduction">
      <name>Introduction</name>
      <t>
        It is increasingly common for an organization (the "IdP-side party")
        authenticated at an Identity Provider to wish to present its users
        with a product surface operated by a second organization (the
        "RP-side party") on an RP-controlled domain, without requiring the
        user to re-authenticate at the RP. The OAuth 2.0 Token Exchange
        specification <xref target="RFC8693"/> provides a mechanism by which
        the IdP-side party's backend can exchange a subject token
        representing the user for an access token usable at the RP. However,
        RFC 8693 is, by its own Section 1, scoped to the server-to-server
        exchange and explicitly leaves usage context out of scope:
      </t>
      <blockquote>
        <t>
          "The security tokens obtained may be used in a number of contexts,
          the specifics of which are also beyond the scope of this
          specification."
        </t>
      </blockquote>
      <t>
        A naive extension of the exchange into the browser leg -- for
        example, placing the RP-issued access token in a redirect URL --
        reintroduces the same class of front-channel token exposure that
        motivated deprecation of the OAuth 2.0 implicit grant. The current
        OAuth 2.0 Security Best Current Practice <xref target="RFC9700"/>
        makes this clear and recommends against transporting access tokens
        in the front channel.
      </t>
      <t>
        This document specifies a profile that composes RFC 8693 with a
        short-lived, single-use authorization code to close the browser-leg
        gap. The profile is intentionally narrow: it does not modify RFC
        8693, does not require changes to the IdP, and adds a single RP-side
        endpoint beyond the standard exchange endpoint. It is inspired by
        the authorization code flow of OAuth 2.0 <xref target="RFC6749"/>
        Section 4.1, adapted for a post-exchange browser handoff rather than
        an end-user authorization step.
      </t>
      <t>
        A secondary contribution of this document is a minimum claims
        contract on the RP-issued access token, enabling the RP to enforce
        authorization statelessly without synchronous introspection or
        out-of-band synchronization with the IdP. This contract is designed
        to be compatible with existing JWT-based access tokens and does not
        require new registered claims beyond those already established in
        <xref target="RFC7519"/> and <xref target="RFC8693"/>.
      </t>

      <section anchor="goals">
        <name>Design Goals</name>
        <t>The profile defined in this document aims to satisfy the following:</t>
        <ol>
          <li>
            The RP-issued access token MUST NOT appear in any URL, browser
            history entry, HTTP Referer header, or intermediary access log
            at any point in the protocol flow.
          </li>
          <li>
            The browser handoff artifact MUST be single-use and time-bound
            such that replay beyond a narrow window is not useful to an
            attacker.
          </li>
          <li>
            The RP MUST be able to enforce authorization decisions using the
            issued access token alone, without synchronous callbacks to the
            IdP.
          </li>
          <li>
            The IdP remains the sole source of truth for user identity,
            tenancy, and permissions.
          </li>
          <li>
            The profile MUST compose with unmodified RFC 8693 token exchange
            semantics.
          </li>
        </ol>
      </section>

      <section anchor="non-goals">
        <name>Non-Goals</name>
        <t>The following are explicitly out of scope for this document:</t>
        <ul>
          <li>
            Real-time revocation propagation for active sessions. Section
            <xref target="revocation" format="counter"/> discusses accepted
            staleness and points to out-of-band mechanisms.
          </li>
          <li>
            RP-originated authorization semantics that the IdP has no
            knowledge of. A provisioning-based synchronization (for example,
            via SCIM) is an appropriate companion mechanism and is left to
            the deployment.
          </li>
          <li>
            Cryptographic token binding mechanisms beyond those provided by
            TLS and standard JWT validation.
          </li>
        </ul>
      </section>

      <section anchor="notational-conventions">
        <name>Requirements Language</name>
        <t>
          The key words "<bcp14>MUST</bcp14>", "<bcp14>MUST NOT</bcp14>",
          "<bcp14>REQUIRED</bcp14>", "<bcp14>SHALL</bcp14>",
          "<bcp14>SHALL NOT</bcp14>", "<bcp14>SHOULD</bcp14>",
          "<bcp14>SHOULD NOT</bcp14>", "<bcp14>RECOMMENDED</bcp14>",
          "<bcp14>NOT RECOMMENDED</bcp14>", "<bcp14>MAY</bcp14>", and
          "<bcp14>OPTIONAL</bcp14>" in this document are to be interpreted
          as described in BCP 14 <xref target="RFC2119"/>
          <xref target="RFC8174"/> when, and only when, they appear in all
          capitals, as shown here.
        </t>
      </section>

      <section anchor="terminology">
        <name>Terminology</name>
        <t>
          This document uses the terms "access token", "authorization
          server", "client", "resource server", and "token endpoint" as
          defined in <xref target="RFC6749"/>, and the terms "Claim" and
          "JWT Claims Set" as defined in <xref target="RFC7519"/>. In
          addition, the following terms are defined:
        </t>
        <dl newline="true">
          <dt>IdP (Identity Provider)</dt>
          <dd>
            The organization and associated infrastructure at which the end
            user is already authenticated. The IdP operates the
            authorization server that issues the subject token presented in
            the RFC 8693 exchange.
          </dd>
          <dt>IdP Backend</dt>
          <dd>
            A server-side component operated by the IdP-side organization
            that initiates the token exchange on behalf of an authenticated
            end user.
          </dd>
          <dt>RP (Relying Party)</dt>
          <dd>
            The organization and associated infrastructure operating the
            destination product surface.
          </dd>
          <dt>RP STS</dt>
          <dd>
            The Security Token Service operated by the RP. Implements the
            RFC 8693 token exchange grant type and issues RP-scoped access
            tokens.
          </dd>
          <dt>RP Session Endpoint</dt>
          <dd>
            An RP-operated HTTP endpoint that accepts a handoff code from
            the browser and establishes an authenticated session.
          </dd>
          <dt>Handoff Code</dt>
          <dd>
            A short-lived, single-use, opaque string generated by the RP
            that indirectly references a cached RP-issued access token.
          </dd>
          <dt>RP Access Token</dt>
          <dd>
            The access token issued by the RP STS in response to the RFC
            8693 exchange. In this profile, it is a JWT
            <xref target="RFC7519"/> signed by the RP STS and verifiable via
            a JWKS <xref target="RFC7517"/> published by the RP STS.
          </dd>
        </dl>
      </section>
    </section>

    <!-- =================================================== -->
    <section anchor="overview">
      <name>Protocol Overview</name>
      <t>
        The profile has two trust legs: a server-to-server exchange (Leg A)
        and a browser-mediated handoff (Legs B and C). The RP access token
        is present only in server-side contexts from the moment of issuance
        through the establishment of the browser session.
      </t>

      <figure anchor="fig-overview">
        <name>High-Level Flow</name>
        <artwork align="left" type="ascii-art"><![CDATA[
    +------------+   (A) RFC 8693    +------------+
    | IdP        | -----------------> | RP STS     |
    | Backend    | <-----------------  |            |
    +-----+------+   RP access token  +-----+------+
          |                                 |
          | (B) redirect with code          | (B) cache token
          |                                 |     under code
          v                                 v
    +------------+                   +------------+
    |            |   (C) POST code    | RP Session |
    |  Browser   | ------------------> | Endpoint   |
    |            | <------------------ |            |
    +------------+   Set-Cookie        +------------+
        ]]></artwork>
      </figure>

      <t>
        <strong>Leg A</strong> is a standard RFC 8693 exchange: the IdP
        Backend presents a subject token obtained from the IdP to the RP
        STS. The RP STS validates the subject token against the IdP's
        published JWKS, applies policy, and returns an RP access token.
      </t>
      <t>
        <strong>Leg B</strong> introduces the handoff code. The RP (either
        as part of the exchange response or via a separate endpoint; see
        <xref target="issuance-shape"/>) generates a handoff code and
        stores the RP access token in a server-side cache keyed by the
        code. The IdP Backend then responds to the user's browser with an
        HTTP redirect whose URL carries only the code.
      </t>
      <t>
        <strong>Leg C</strong> is redemption. The browser, now on the
        RP-controlled domain, POSTs the code to the RP Session Endpoint.
        The RP atomically retrieves and invalidates the cached access
        token, establishes a server-side session, and sets a session
        cookie scoped to the RP domain.
      </t>
    </section>

    <!-- =================================================== -->
    <section anchor="exchange">
      <name>Token Exchange (Leg A)</name>

      <section anchor="trust-setup">
        <name>Trust Setup</name>
        <t>
          The IdP <bcp14>MUST</bcp14> publish a JWKS
          <xref target="RFC7517"/> at a stable URL. The RP STS
          <bcp14>MUST</bcp14> retrieve and cache this JWKS and use it to
          validate subject token signatures. Symmetric keys
          (<bcp14>MUST NOT</bcp14>) be used for subject token signing in
          this profile.
        </t>
        <t>
          The RP STS <bcp14>MUST</bcp14> publish its own JWKS at a stable
          URL for validation of RP-issued access tokens by downstream RP
          components.
        </t>
        <t>
          The IdP Backend <bcp14>MUST</bcp14> authenticate to the RP STS
          when initiating an exchange. <xref target="RFC7523"/> client
          authentication using a private key
          (<tt>private_key_jwt</tt>) is <bcp14>RECOMMENDED</bcp14>.
          <tt>client_secret_basic</tt> <bcp14>MAY</bcp14> be used in
          non-production environments but <bcp14>SHOULD NOT</bcp14> be
          used in production deployments.
        </t>
      </section>

      <section anchor="exchange-request">
        <name>Exchange Request</name>
        <t>The IdP Backend sends an RFC 8693 token exchange request to the RP STS:</t>
        <figure anchor="fig-exchange-req">
          <name>Token Exchange Request</name>
          <sourcecode type="http-message"><![CDATA[
POST /oauth2/token HTTP/1.1
Host: sts.rp.example
Content-Type: application/x-www-form-urlencoded
Authorization: <client authentication per Section 3.1>

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&audience=https://rp.example/
&subject_token=<IdP-issued JWT for the user>
&subject_token_type=urn:ietf:params:oauth:token-type:jwt
&requested_token_type=urn:ietf:params:oauth:token-type:access_token
          ]]></sourcecode>
        </figure>
      </section>

      <section anchor="exchange-response">
        <name>Exchange Response</name>
        <t>
          On success, the RP STS returns a standard RFC 8693 response per
          Section 2.2.1 of <xref target="RFC8693"/>:
        </t>
        <figure anchor="fig-exchange-resp">
          <name>Token Exchange Response</name>
          <sourcecode type="http-message"><![CDATA[
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "access_token": "<RP-issued JWT>",
  "issued_token_type":
      "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 1800
}
          ]]></sourcecode>
        </figure>
      </section>

      <section anchor="sts-validation">
        <name>Validation Performed by the RP STS</name>
        <t>The RP STS <bcp14>MUST</bcp14>, at minimum:</t>
        <ol>
          <li>Validate the IdP Backend's client authentication.</li>
          <li>Validate the signature of the <tt>subject_token</tt> against the IdP's JWKS.</li>
          <li>
            Validate the <tt>iss</tt>, <tt>aud</tt>, <tt>exp</tt>,
            <tt>nbf</tt>, and <tt>iat</tt> claims of the subject token per
            Section 4 of <xref target="RFC7519"/>.
          </li>
          <li>
            Confirm that the subject token was intended for use as a subject
            in this exchange. A dedicated claim value or scope registered
            between IdP and RP at onboarding <bcp14>SHOULD</bcp14> be
            used. Acceptance of arbitrary IdP-issued tokens minted for
            unrelated purposes <bcp14>MUST NOT</bcp14> be permitted.
          </li>
          <li>Apply RP-side policy (rate limits, denylists, tenant eligibility).</li>
          <li>
            Project identity and authorization claims onto the RP access
            token per <xref target="claims"/>.
          </li>
        </ol>
        <t>
          On any validation failure, the RP STS <bcp14>MUST</bcp14> respond
          per Section 2.2.2 of <xref target="RFC8693"/> and
          <bcp14>MUST NOT</bcp14> issue a token.
        </t>
      </section>
    </section>

    <!-- =================================================== -->
    <section anchor="handoff">
      <name>Browser Handoff (Legs B and C)</name>

      <section anchor="code-generation">
        <name>Handoff Code Generation and Storage</name>
        <t>
          On issuing an RP access token intended for browser-based session
          establishment, the RP generates a handoff code and persists the
          mapping from code to RP access token (and associated metadata) in
          a short-lived server-side store.
        </t>
        <t>The handoff code <bcp14>MUST</bcp14> have the following properties:</t>
        <dl newline="false">
          <dt>Opacity:</dt>
          <dd>No information about the user, tenant, or token is derivable from the code's value.</dd>
          <dt>High entropy:</dt>
          <dd>
            At least 128 bits of cryptographically random material,
            base64url-encoded. 256 bits <bcp14>RECOMMENDED</bcp14>.
          </dd>
          <dt>Single-use:</dt>
          <dd>
            The cached mapping <bcp14>MUST</bcp14> be deleted atomically on
            first successful redemption. Any subsequent redemption attempt
            <bcp14>MUST</bcp14> fail.
          </dd>
          <dt>Short-lived:</dt>
          <dd>
            A Time-To-Live (TTL) of 60 seconds is
            <bcp14>RECOMMENDED</bcp14>. TTL <bcp14>MUST NOT</bcp14> exceed
            120 seconds.
          </dd>
          <dt>Client-bound (advisory):</dt>
          <dd>
            A coarse client fingerprint (for example, a User-Agent hash)
            captured at issuance <bcp14>SHOULD</bcp14> be recorded
            alongside the cached entry. On redemption, mismatch
            <bcp14>SHOULD</bcp14> be logged and <bcp14>MAY</bcp14>
            cause rejection. Binding to client IP address is
            <bcp14>NOT RECOMMENDED</bcp14>: mobile carrier NAT,
            corporate egress, and VPN transitions can cause legitimate
            redemptions to originate from a different IP than the
            redirect.
          </dd>
        </dl>
      </section>

      <section anchor="redirect">
        <name>Redirect to the RP Domain</name>
        <t>
          Having obtained a handoff code, the IdP Backend responds to the
          user's browser with an HTTP redirect:
        </t>
        <figure anchor="fig-redirect">
          <name>Redirect to RP</name>
          <sourcecode type="http-message"><![CDATA[
HTTP/1.1 302 Found
Location: https://rp.example/session/handoff?code=<handoff_code>
Cache-Control: no-store
          ]]></sourcecode>
        </figure>
        <t>
          A <tt>state</tt> parameter <bcp14>MAY</bcp14> also be included for
          CSRF protection against the initiating IdP page, per standard
          OAuth 2.0 redirect hygiene.
        </t>
      </section>

      <section anchor="issuance-shape">
        <name>Code Issuance Shape</name>
        <t>Two implementation shapes are conformant; the interface contract is relevant to the IdP-side implementer:</t>
        <t>
          <strong>Combined response:</strong> The RFC 8693 response is
          extended with a handoff code alongside the access token. This
          minimizes round trips but requires the IdP Backend to handle a
          non-standard response extension.
        </t>
        <t>
          <strong>Separate issuance endpoint:</strong> After the RFC 8693
          exchange, the IdP Backend calls a second RP endpoint, passing the
          RP access token and receiving a handoff code. This is cleaner
          separation between the standard (RFC 8693) and profile-specific
          concerns and is the <bcp14>RECOMMENDED</bcp14> shape.
        </t>
      </section>

      <section anchor="redemption">
        <name>Code Redemption</name>
        <t>
          The browser, having arrived at the RP handoff page, client-side
          code on that page <bcp14>MUST</bcp14> immediately POST the code
          to the RP Session Endpoint:
        </t>
        <figure anchor="fig-redeem">
          <name>Redemption Request</name>
          <sourcecode type="http-message"><![CDATA[
POST /session/redeem HTTP/1.1
Host: rp.example
Content-Type: application/json
Origin: https://rp.example

{"code": "<handoff_code>"}
          ]]></sourcecode>
        </figure>
        <t>
          The request is same-origin. The RP Session Endpoint
          <bcp14>MUST</bcp14> verify the <tt>Origin</tt> header.
        </t>
      </section>

      <section anchor="session-establishment">
        <name>Session Establishment</name>
        <t>On receiving a redemption request, the RP Session Endpoint <bcp14>MUST</bcp14>:</t>
        <ol>
          <li>Atomically retrieve and delete the code's cached mapping.</li>
          <li>Validate the cached RP access token's <tt>exp</tt>.</li>
          <li>Validate the optional client binding from <xref target="code-generation"/>.</li>
          <li>
            Create a server-side session record keyed by a server-generated
            session identifier and store the access token's claims as the
            session's authorization context.
          </li>
          <li>Set the session cookie:</li>
        </ol>
        <figure anchor="fig-set-cookie">
          <name>Redemption Response (Success)</name>
          <sourcecode type="http-message"><![CDATA[
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: rp_session=<session_id>;
            Domain=rp.example;
            Path=/;
            HttpOnly;
            Secure;
            SameSite=Lax;
            Max-Age=1800
Cache-Control: no-store

{"redirect": "/app/home"}
          ]]></sourcecode>
        </figure>
        <t>
          The cookie <bcp14>MUST</bcp14> carry the <tt>HttpOnly</tt> and
          <tt>Secure</tt> attributes. <tt>SameSite=Lax</tt> is the
          <bcp14>RECOMMENDED</bcp14> default; <tt>SameSite=Strict</tt>
          <bcp14>SHOULD</bcp14> be used where the post-login flow permits.
        </t>
      </section>

      <section anchor="failure-modes">
        <name>Failure Modes</name>
        <t>
          On any redemption failure -- unknown code, already-consumed code,
          expired code, client binding mismatch, expired cached token --
          the RP Session Endpoint <bcp14>MUST</bcp14> return a generic
          error response. Distinguishing failure reasons in the response
          body <bcp14>MUST NOT</bcp14> occur, to avoid enabling
          code-probing oracles. Distinct failure reasons
          <bcp14>SHOULD</bcp14> be recorded in server logs with
          correlation identifiers for operational visibility.
        </t>
      </section>
    </section>

    <!-- =================================================== -->
    <section anchor="claims">
      <name>Access Token Claims Contract</name>
      <t>
        This section specifies the minimum set of claims the RP-issued
        access token <bcp14>MUST</bcp14> carry to support stateless
        authorization enforcement at the RP.
      </t>

      <section anchor="required-claims">
        <name>Required Claims</name>
        <t>
          In addition to the standard JWT claims <tt>iss</tt>,
          <tt>sub</tt>, <tt>aud</tt>, <tt>exp</tt>, <tt>iat</tt>, and
          <tt>jti</tt> defined in <xref target="RFC7519"/>, the following
          claims <bcp14>MUST</bcp14> appear:
        </t>
        <dl newline="false">
          <dt><tt>tenant_id</tt> (string):</dt>
          <dd>
            The tenancy context in which the user is acting. The RP
            <bcp14>MUST</bcp14> scope all authorization decisions and
            data access to this value for the session lifetime.
          </dd>
          <dt><tt>perms</tt> (array of strings):</dt>
          <dd>
            A flat list of permission strings representing authorization
            grants within the tenant. The RP <bcp14>MUST</bcp14> enforce
            access decisions against this set. See
            <xref target="perms-format"/>.
          </dd>
          <dt><tt>scope</tt> (string):</dt>
          <dd>
            Space-delimited scope values per Section 3.3 of
            <xref target="RFC6749"/>, constraining what the access token
            itself is usable for.
          </dd>
        </dl>
      </section>

      <section anchor="perms-format">
        <name>Permissions Format</name>
        <t>
          Permissions <bcp14>SHOULD</bcp14> be expressed as colon-delimited
          <tt>resource:action</tt> strings, for example
          <tt>reports:read</tt>, <tt>records:write</tt>,
          <tt>admin:users:read</tt>.
        </t>
        <t>
          This shape is preferred over named roles because it allows the RP
          to enforce directly without maintaining a role-to-permission
          mapping. Where the IdP's authorization model is role-based, a
          snapshot of derived permissions <bcp14>SHOULD</bcp14> be
          materialized into the <tt>perms</tt> claim at token mint time;
          a <tt>roles</tt> claim <bcp14>MAY</bcp14> additionally be
          included for informational purposes, but the RP
          <bcp14>MUST</bcp14> enforce against <tt>perms</tt>.
        </t>
      </section>

      <section anchor="optional-claims">
        <name>Optional Claims</name>
        <t>The following claims <bcp14>MAY</bcp14> appear:</t>
        <dl newline="false">
          <dt><tt>email</tt>, <tt>name</tt>:</dt>
          <dd>User profile information for display. Not authorization-relevant.</dd>
          <dt><tt>act</tt>:</dt>
          <dd>
            Per Section 4.1 of <xref target="RFC8693"/>. Included if the
            exchange involves delegation. The RP <bcp14>MUST</bcp14> apply
            access control decisions considering the actor, per RFC 8693
            Section 4.1.
          </dd>
        </dl>
      </section>

      <section anchor="lifetime">
        <name>Token and Session Lifetime</name>
        <t>
          The access token <tt>exp</tt> <bcp14>SHOULD</bcp14> be between 15
          and 60 minutes. Shorter values improve the timeliness of
          authorization changes at the cost of exchange volume. The RP
          session cookie lifetime <bcp14>MAY</bcp14> exceed the access
          token lifetime if the RP implements silent re-exchange; such a
          mechanism is out of scope for this document.
        </t>
      </section>
    </section>

    <!-- =================================================== -->
    <section anchor="security-considerations">
      <name>Security Considerations</name>

      <section anchor="sec-front-channel">
        <name>Front-Channel Exposure</name>
        <t>
          The primary motivation for this profile is to prevent the RP
          access token from appearing in any front-channel context. This
          profile ensures the access token is present only in: the RFC 8693
          response over TLS; the RP-side cache keyed by the handoff code;
          and the RP-side session record. The token does not appear in any
          URL, browser storage, HTTP Referer header, or intermediary log.
        </t>
      </section>

      <section anchor="sec-code-exposure">
        <name>Handoff Code Exposure</name>
        <t>
          The handoff code appears in a URL and is therefore exposed to
          browser history, access logs at the RP domain, and (potentially)
          Referer headers if the handoff page links outward. Because the
          code is single-use and short-lived, exposure after consumption
          is harmless. Exposure during the redemption window is the only
          attack surface.
        </t>
        <t>Mitigations:</t>
        <ul>
          <li>TTL not exceeding 120 seconds compresses the attack window.</li>
          <li>Single-use ensures no persistent exposure.</li>
          <li>TLS everywhere removes network-level observation.</li>
          <li>The optional client binding check detects user-agent substitution.</li>
          <li>
            The handoff page <bcp14>MUST</bcp14> set
            <tt>Referrer-Policy: no-referrer</tt>.
          </li>
        </ul>
      </section>

      <section anchor="sec-subject-misuse">
        <name>Subject Token Misuse</name>
        <t>
          The RP STS <bcp14>MUST</bcp14> validate that the presented
          subject token was intended for use in an exchange to the RP.
          An IdP-issued token minted for an unrelated purpose
          <bcp14>MUST NOT</bcp14> be accepted. Acceptance
          <bcp14>SHOULD</bcp14> be gated on an explicit <tt>aud</tt>
          value or dedicated scope agreed between IdP and RP at
          onboarding.
        </t>
      </section>

      <section anchor="sec-handoff-page">
        <name>Handoff Page Hygiene</name>
        <t>The RP handoff page <bcp14>MUST</bcp14>:</t>
        <ul>
          <li>Set <tt>Referrer-Policy: no-referrer</tt>.</li>
          <li>Set <tt>Cache-Control: no-store</tt>.</li>
          <li>Contain no third-party scripts, trackers, or external resources.</li>
          <li>Perform the redemption POST immediately on load, without user interaction.</li>
          <li>On failure, redirect to a generic error page without reflecting the code or derived information in the URL.</li>
        </ul>
      </section>

      <section anchor="sec-cookies">
        <name>Session Cookie Attributes</name>
        <t>The session cookie <bcp14>MUST</bcp14> be set with:</t>
        <ul>
          <li><tt>HttpOnly</tt>, preventing JavaScript access.</li>
          <li><tt>Secure</tt>, preventing transmission over plaintext HTTP.</li>
          <li><tt>SameSite=Lax</tt> or <tt>SameSite=Strict</tt>.</li>
          <li>A <tt>Domain</tt> attribute scoped to the RP origin only.</li>
          <li>A conservative <tt>Max-Age</tt> aligned with the session lifetime.</li>
        </ul>
      </section>

      <section anchor="sec-tls">
        <name>Transport Security</name>
        <t>
          All HTTP interactions in this profile <bcp14>MUST</bcp14> use
          TLS 1.2 or higher. Plaintext HTTP <bcp14>MUST</bcp14> be rejected
          at all endpoints.
        </t>
      </section>

      <section anchor="sec-clock-skew">
        <name>Clock Skew</name>
        <t>
          RP STS, IdP Backend, and IdP clocks <bcp14>SHOULD</bcp14> be
          synchronized. A small skew tolerance of 30 to 60 seconds
          <bcp14>SHOULD</bcp14> be applied when validating
          <tt>exp</tt>, <tt>nbf</tt>, and <tt>iat</tt>. The handoff
          code TTL <bcp14>SHOULD NOT</bcp14> be inflated to absorb
          clock skew.
        </t>
      </section>

      <section anchor="sec-rate-limiting">
        <name>Rate Limiting</name>
        <t>
          The RP STS <bcp14>SHOULD</bcp14> rate-limit exchange requests per
          authenticated client. The RP Session Endpoint <bcp14>MUST</bcp14>
          rate-limit redemption attempts per source IP to defend against
          code-probing, notwithstanding the impracticality of guessing
          high-entropy codes.
        </t>
      </section>

      <section anchor="revocation">
        <name>Revocation and Staleness</name>
        <t>
          Because the RP enforces authorization from the access token
          alone, permission changes made at the IdP between token-mint
          events are not visible to the RP until the next exchange.
          Deployments that require near-real-time revocation
          <bcp14>SHOULD</bcp14> adopt one of: OAuth 2.0 Token
          Introspection <xref target="RFC7662"/>; a revocation signal
          channel (for example, webhook or event stream); or
          sufficiently short token lifetimes combined with silent
          re-exchange. Selection among these is a deployment concern
          and is out of scope for this profile.
        </t>
      </section>

      <section anchor="sec-general">
        <name>General OAuth Security Considerations</name>
        <t>
          The security considerations of <xref target="RFC6749"/>,
          <xref target="RFC6819"/>, <xref target="RFC8693"/>, and
          <xref target="RFC9700"/> apply in full to this profile.
        </t>
      </section>
    </section>

    <!-- =================================================== -->
    <section anchor="privacy">
      <name>Privacy Considerations</name>
      <t>
        The RP access token and the subject token may both carry
        privacy-sensitive information about the end user, including
        identifiers, tenancy, and permissions. Both
        <bcp14>MUST</bcp14> be transmitted only over TLS.
        Deployments <bcp14>SHOULD</bcp14> practice data minimization:
        the <tt>perms</tt> claim should contain only permissions
        relevant to the RP's scope, not the user's full rights at the
        IdP.
      </t>
      <t>
        The handoff code itself carries no personal information and is
        not a privacy concern beyond the exposure of its existence in
        URL contexts.
      </t>
    </section>

    <!-- =================================================== -->
    <section anchor="iana">
      <name>IANA Considerations</name>
      <t>
        This document has no IANA actions. It does not define new
        grant types, token type identifiers, JWT claims, or OAuth
        parameters. All identifiers used are drawn from existing
        registrations established by
        <xref target="RFC6749"/>, <xref target="RFC7519"/>, and
        <xref target="RFC8693"/>.
      </t>
    </section>

  </middle>

  <back>
    <references>
      <name>References</name>

      <references>
        <name>Normative References</name>

        <reference anchor="RFC2119" target="https://www.rfc-editor.org/info/rfc2119">
          <front>
            <title>Key words for use in RFCs to Indicate Requirement Levels</title>
            <author initials="S." surname="Bradner" fullname="S. Bradner"><organization/></author>
            <date year="1997" month="March"/>
          </front>
          <seriesInfo name="BCP" value="14"/>
          <seriesInfo name="RFC" value="2119"/>
          <seriesInfo name="DOI" value="10.17487/RFC2119"/>
        </reference>

        <reference anchor="RFC6749" target="https://www.rfc-editor.org/info/rfc6749">
          <front>
            <title>The OAuth 2.0 Authorization Framework</title>
            <author initials="D." surname="Hardt" fullname="D. Hardt" role="editor"><organization/></author>
            <date year="2012" month="October"/>
          </front>
          <seriesInfo name="RFC" value="6749"/>
          <seriesInfo name="DOI" value="10.17487/RFC6749"/>
        </reference>

        <reference anchor="RFC7517" target="https://www.rfc-editor.org/info/rfc7517">
          <front>
            <title>JSON Web Key (JWK)</title>
            <author initials="M." surname="Jones" fullname="M. Jones"><organization/></author>
            <date year="2015" month="May"/>
          </front>
          <seriesInfo name="RFC" value="7517"/>
          <seriesInfo name="DOI" value="10.17487/RFC7517"/>
        </reference>

        <reference anchor="RFC7519" target="https://www.rfc-editor.org/info/rfc7519">
          <front>
            <title>JSON Web Token (JWT)</title>
            <author initials="M." surname="Jones" fullname="M. Jones"><organization/></author>
            <author initials="J." surname="Bradley" fullname="J. Bradley"><organization/></author>
            <author initials="N." surname="Sakimura" fullname="N. Sakimura"><organization/></author>
            <date year="2015" month="May"/>
          </front>
          <seriesInfo name="RFC" value="7519"/>
          <seriesInfo name="DOI" value="10.17487/RFC7519"/>
        </reference>

        <reference anchor="RFC7523" target="https://www.rfc-editor.org/info/rfc7523">
          <front>
            <title>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants</title>
            <author initials="M." surname="Jones" fullname="M. Jones"><organization/></author>
            <author initials="B." surname="Campbell" fullname="B. Campbell"><organization/></author>
            <author initials="C." surname="Mortimore" fullname="C. Mortimore"><organization/></author>
            <date year="2015" month="May"/>
          </front>
          <seriesInfo name="RFC" value="7523"/>
          <seriesInfo name="DOI" value="10.17487/RFC7523"/>
        </reference>

        <reference anchor="RFC7662" target="https://www.rfc-editor.org/info/rfc7662">
          <front>
            <title>OAuth 2.0 Token Introspection</title>
            <author initials="J." surname="Richer" fullname="J. Richer" role="editor"><organization/></author>
            <date year="2015" month="October"/>
          </front>
          <seriesInfo name="RFC" value="7662"/>
          <seriesInfo name="DOI" value="10.17487/RFC7662"/>
        </reference>

        <reference anchor="RFC8174" target="https://www.rfc-editor.org/info/rfc8174">
          <front>
            <title>Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words</title>
            <author initials="B." surname="Leiba" fullname="B. Leiba"><organization/></author>
            <date year="2017" month="May"/>
          </front>
          <seriesInfo name="BCP" value="14"/>
          <seriesInfo name="RFC" value="8174"/>
          <seriesInfo name="DOI" value="10.17487/RFC8174"/>
        </reference>

        <reference anchor="RFC8693" target="https://www.rfc-editor.org/info/rfc8693">
          <front>
            <title>OAuth 2.0 Token Exchange</title>
            <author initials="M." surname="Jones" fullname="M. Jones"><organization/></author>
            <author initials="A." surname="Nadalin" fullname="A. Nadalin"><organization/></author>
            <author initials="B." surname="Campbell" fullname="B. Campbell" role="editor"><organization/></author>
            <author initials="J." surname="Bradley" fullname="J. Bradley"><organization/></author>
            <author initials="C." surname="Mortimore" fullname="C. Mortimore"><organization/></author>
            <date year="2020" month="January"/>
          </front>
          <seriesInfo name="RFC" value="8693"/>
          <seriesInfo name="DOI" value="10.17487/RFC8693"/>
        </reference>
      </references>

      <references>
        <name>Informative References</name>

        <reference anchor="RFC6819" target="https://www.rfc-editor.org/info/rfc6819">
          <front>
            <title>OAuth 2.0 Threat Model and Security Considerations</title>
            <author initials="T." surname="Lodderstedt" fullname="T. Lodderstedt" role="editor"><organization/></author>
            <author initials="M." surname="McGloin" fullname="M. McGloin"><organization/></author>
            <author initials="P." surname="Hunt" fullname="P. Hunt"><organization/></author>
            <date year="2013" month="January"/>
          </front>
          <seriesInfo name="RFC" value="6819"/>
          <seriesInfo name="DOI" value="10.17487/RFC6819"/>
        </reference>

        <reference anchor="RFC9700" target="https://www.rfc-editor.org/info/rfc9700">
          <front>
            <title>Best Current Practice for OAuth 2.0 Security</title>
            <author initials="T." surname="Lodderstedt" fullname="T. Lodderstedt"><organization/></author>
            <author initials="J." surname="Bradley" fullname="J. Bradley"><organization/></author>
            <author initials="A." surname="Labunets" fullname="A. Labunets"><organization/></author>
            <author initials="D." surname="Fett" fullname="D. Fett"><organization/></author>
            <date year="2025" month="January"/>
          </front>
          <seriesInfo name="BCP" value="240"/>
          <seriesInfo name="RFC" value="9700"/>
          <seriesInfo name="DOI" value="10.17487/RFC9700"/>
        </reference>
      </references>
    </references>

    <section anchor="acknowledgements" numbered="false">
      <name>Acknowledgements</name>
      <t>
        This profile draws directly on the design patterns of
        <xref target="RFC8693"/> and Section 4.1 of
        <xref target="RFC6749"/>. Reviewers are thanked for their
        feedback, which motivated the explicit separation of the
        server-to-server exchange from the browser handoff.
      </t>
    </section>
  </back>
</rfc>
