OAuth 2.0 and OIDC State Handling
In OAuth 2.0, the state parameter included in authorization requests and returned by the Authorization Server (AS) in authorization responses is used to [4]:
maintain Client state between authorization request and authorization response
protect against request forgery, primarily cross-site request forgery (CSRF).
The state parameter is used identically in an OpenID Connect (OIDC) interaction between an OIDC Relaying Party (RP) and an OpenID Provider (OP).
State for maintaining application context
The OAuth 2.0 state parameter included in authorization requests is returned unchanged by the Authorization Server or OpenID Provider (analogous to SAML RelayState). The value of state is defined as a VSCHAR string (all printable ASCII characters, including space but excluding control characters). It should (RFC6749) be used to provide CSRF protection (see the next section) and may additionally carry client‑local application state required to restore application context after the authorization response is received.
The structure and encoding of information carried in the state parameter are implementation‑specific; the RP/Client is responsible for both generating and consuming its own state. As a result, the intended audience of the state value is the client that created it, and the Authorisation Server or OpenID Provider treats it as opaque. Some attempts have been made in the IETF to formalise state encoding by using a tamper‑proof, confidential, stateless JWT [3]. Some implementations use a nonce value that both provides CSRF protection and serves as a reference key to client‑side or server‑side stored state (for example, in a cookie or session storage) [6].
State for CSRF protection
The state parameter should encode a random value (nonce), known as a CSRF token [2] or a Request Forgery Protection (rfp) value [3], to protect against cross-site request forgery. Without protection, an attacker can trick a legitimate user into using their authorization code, a type of login CSRF [5]. Noting here, there are no unsolicited flows in OAuth or OIDC (AFAIK, although there is some mention of it in [3]).
The CSRF token should be linked to the user agent session. This can be achieved by storing the CSRF token value sent in the request using either session storage on the server or by using an HTTP-Only, Secure cookie.
Alternative measures for protecting against authorization code interception include Proof Key for Code Exchange (PKCE) [1]. Additionally, if using OIDC, the ID Token can be protected from replay by adding a nonce to the authentication request and matching it to that returned in the ID Token. RFC9700 suggests that both PKCE and OIDC nonce values provide the same protection as a CSRF token in the state parameter (indirectly, I guess they do—they do not directly protect/correlate the request and response). In all cases, the values sent in the authorization/authentication request need to be stored by the RP/Client so they can be matched against those returned in the response.
State handling in the OIDC SP
Using OAuth 2.0 state to recover application state is important as the Hub is intended to be as stateless as possible. Therefore, we need a way to capture and restore the authentication context between a single, correlated, asynchronous request and response. How this is achieved, alongside providing CSRF defence, is shown below.
Session Initiation
(1) On session initiation, either the Agent supplies a state value in the DDF, or MapResourceToStateToken action sets one onto the DDF from the state token generated by the StateTokenManager when preserving the resource URL value. In either case, we refer to that state as DDF.state.
Is it possible that we do not want this value to come from the agent? (This should be OK because of the RFP construction in step (2).)
(2) The OAuth 2.0 request state is created as the following JSON object, which is base64URL encoded and added to the authorization request. For production use, the value is ‘sealed’ using the DataSealer to protect the values from tampering.
{"state":"<DDF.state>","rfp":"dde562dbcad1b71948a7ce3858d95ea7"}The
statevalue is theDDF.statefrom step (1).The
rfpvalue is a cryptographically random nonce that protects against CSRF.The
rfpclaim name defined in the expired RFC draft [3] is used to avoid confusing it with the ‘nonce' inside the ID Token.The
rfpvalue does not come from the DDFstatebecause the RP can not guarantee the entropy of that value (it isn’t intended for that).
For example, the following shows a sealed state parameter in an authorization request:
https://op.example.com/authorize?client_id=mytestclient&response_type=code&scope=openid&response_mode=query&state=QUFkelpXTnlaWFF4dW80Vko3RWk1SytGMHMxdCtSdjRnR25ldHpsNlNER01hOU83YmlQZlhhTGhnaXdiY3QycWNrYjZ5dmpGRXU5ZVVaRmtqSFJWMTlwY3h2QXNpR2tBcVNLQXV6bmJBWjR3SDdpaEtwTlhheU9JMGt2UEFIQS9ESDVnRlNZT0h2Qm04b1BzYW9pZ05RdDlBQUEvZEVzM2ZyVE9DYmpkSlRBRXNpaWwzSnVsRzhUTFhMYzVtMEFvclhERi9nOD0(3) Any state from the authorization and authentication context that needs to be preserved between request and response is saved off using the StateTokenManager. It is stored as a sealed JSON object, an example is provided below (the exact values stored may change):
{"client_id":"testspclientid","nonce":"a6d7b8f6106b3587","is_auth_time_required":false,"requested_acrs":[],"redirect_uri":"https://sp.example.org/Shibboleth.sso/callback","authority":"https://op.example.org"}(4) The state token in the OAuth 2.0 state parameter from step (2) also serves as a reference/key to an HTTP-only, secure correlation cookie that encodes a sealed JSON object:
{"authnState":"1775745647154_15c025e6ff0e4375f219a32517476e1c","rfp":"dde562dbcad1b71948a7ce3858d95ea7"}authnStateis a state token, generated by theStateTokenManagerwhen the authentication context was preserved in step (3).rfpshould match therfpvalue in the OAuthstateparameter to prevent CSRF.
(outcomes)
As a result, we now have:
DDF.State, from the Agent or generated byMapResourceToStateTokenOAuth 2.0 state as
{DDF.State, RFP-Nonce}sent to, and returned by, the AS/OPPossibly a state manager mapping from
DDF.State→ResourceURL. (eventually cleared after use)A cookie manager mapping from
DDF.State→ correlation cookie{authnState, RFP-Value}. (eventually unset after use)A state manager mapping from
authnState--> authentication context{authority, nonce, …}. (eventually cleared after use)
Action Class Changes
The following action classes were added to the OIDC SP:
MapStateValueToStateToken, which is based on
MapResourceToStateTokenDefaults
errorFatalto true, as this is more critical in the OIDC case.Would need a suitable
doPreExecuteto be compatible with the sp-server version
IssueStateCookie, which is a slightly generalised version of
IssueCorrelationCookieIs only used to issue a ‘correlation’ cookie, so maybe the original naming is better.
ResolveStateCookie, which is based on
ProcessCorrelationCookieAgain, it is only resolving a correlation cookie.
MapStateTokenToStateValue, which is based on
MapStateTokenToResourceWould need a suitable
doPreExecuteto be compatible with the sp-server version
Token Consumption
(1) The inbound DecodeMessage stage decodes and unseals the OAuth 2.0 stateparameter and sets it onto the OAuthStateContext. Fail on error.
(2) The DDF.Statein the OAuth 2.0 decoded state JSON is used to recover the resource URL from the state manager (if present). Clear after use.
(3) The DDF.Statein the OAuth 2.0 decoded state JSON is used to retrieve the correlation cookie from the cookie manager, this is decoded, unsealed and set onto the CorrelationCookieStateContext. Once unsealed unset cookie to prevent re-use.
(4) The authnState from the correlation cookie is used to recover the authentication context from the state manager. Clear state to prevent re-use.
(5) The rfp value in the correlation cookie is matched to the rfp value in the decoded OAuth 2.0 state JSON parameter—to prevent CSRF. Fail on error.
We perform the CSRF check after we retrieve state in steps (2), (3), and (4) so they can be unset/cleared even if CSRF validation fails. Importantly, though, the CSRF check occurs before the values are actually used in further steps.
(6) Processing continues using the decoded authentication context from step (4).
Still Todo
Add a replay cache check for the
rfp.
References
[1] OAuth Best Practices: https://datatracker.ietf.org/doc/html/rfc9700
[2] OAuth Threat Model and Security Considerations: https://www.rfc-editor.org/rfc/rfc6819
[3] JWT encoded state: https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09
[4] The OAuth 2.0 Authorization Framework: https://www.rfc-editor.org/rfc/rfc6749
[5] OWASP CSRF: https://owasp.org/www-community/attacks/csrf
[6] https://auth0.com/docs/secure/attack-protection/state-parameters