CORS response mode
This guide describes the configuration and setup of a custom CORS response mode
to handle silent prompt=none
authorisation requests from browser-based
applications, also known as Single Page
Applications (SPA).
1. Background
An SPA that signs-in users with OpenID Connect or obtains access tokens via OAuth 2.0 can have the need to periodically check with the Connect2id server if the person still has a session or to get a new access token in the absence of a long-lived refresh token credential.
To do this the SPA can make an authorisation request
with the optional prompt=none
parameter. This parameter tells the Connect2id
server to try to fulfill the request silently, without any user interaction,
and proceed directly to the authorisation response.
If the server doesn’t find a valid session for the user or no recorded
(long-lived) consent for the requesting client, it will return a
login_required
, consent_required
or interaction_required
error.
https://c2id.com/login?
response_type=code
&scope=openid
&client_id=123
&state=af0ifjsldkj
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
&prompt=none
&id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlO...
The prompt
parameter is part of the OpenID Connect specification, but due to
its general usefulness the Connect2id server also supports it in plain OAuth
2.0 authorisation requests.
For an SPA that is already loaded doing periodic top-level browser redirections
to the Connect2id server authorisation endpoint for a prompt=none
request
normally isn’t acceptable as it breaks the user experience. Performing the
redirection in a hidden iframe
doesn’t normally work for an SPA either.
The most natural and convenient solution for an SPA is therefore to make a cross-origin (CORS) fetch call to the authorisation endpoint.
The authorisation endpoint of the Connect2id server will need to see any
previously set session cookie in the CORS request, so the fetch call must be
created with the “include credentials” parameter. There is a snag however – on
the 303 redirection to
the redirect_uri
of the SPA however the CORS request is going to fail,
because the browser is obliged to null the Origin
header on a redirection.
This means we will need an authorisation response where its parameters are
returned in the HTTP entity body, preferably in a JSON object. With a straight
GET call all CORS parameters can now successfully pass. Enter our new custom
cors
response mode.
2. Connect2id server configuration
Let’s call this new custom OAuth 2.0 response mode cors
.
Declare it in the op.authz.responseModes configuration property so that the Connect2id server will accept it when an OAuth client requests it. Append it at end of the response modes list:
op.authz.responseModes=query,...,cors
To apply the necessary logic for processing prompt=none
requests with
response_mode=cors
also add these two configurations:
To intercept prompt=none
requests in the login page:
op.authz.alwaysPromptForAuth=true
To include the
redirect_uri
, prompt
and response_mode
request parameters at the
authentication step:
op.authz.requestParamsInAuthPrompt=prompt,response_mode
Configure the Connect2id server to require an ID token
hint for all
prompt=none
requests. Since the ID token is
an OpenID Connect object this will prevent plain OAuth 2.0 authorisation
requests with prompt=none
.
op.authz.requireIDTokenHintWithPromptNone=true
If you need to process plain prompt=none
authorisation requests two or more
clients must not have the same web origin (host / domain name) in the
registeredredirection_uris
. The security
section explains why.
3. Client permission to use the CORS response mode
Since there is no standard
client metadata to flag which
OAuth clients are permitted to use the CORS response mode we are going to
define our own, inside the custom data
JSON object.
Example registration for an SPA that is a public OAuth client, meaning it doesn’t store credentials for authenticating at the token endpoint:
{
"redirect_uris" : [ "https://client.example.org/cb" ],
"token_endpoint_auth_method" : "none",
"data" : {
"allow_response_mode_cors" : true
}
}
The explicit client permission to use the CORS response mode is needed to block potential CORS requests from other client applications in case those suffer some attack. Such an attack can be especially devastating if there are clients using the implicit flow, deprecated in OAuth 2.1, where the access token gets returned via the front-end.
4. Login page
The login page sits on top of the Connect2id server authorisation session API and is responsible for the login and consent UI among other things.
4.1 Cookie policy
The login page should set the
policy
of the issued session cookies to SameSite=None; Secure
. This will instruct
the browser to include the authorisation endpoint cookie in CORS requests when
the “include credentials” parameter is set. If the SameSite policy is more
restrictive no cookies will be included in CORS requests, even if the
credentials parameter is set.
Note, the SameSite policy is a crucial measure for stopping CSRF attacks.
When relaxing the SameSite policy from the default Lax
to None
to allow
CORS with credentials, sufficient other measures must be in place to guard
against CSRF. See the OWASP
cheatsheet
for suggestions.
For extra security the login page may choose to have a default
SameSite=Lax
policy and relax it to None
only when a client flagged for the
CORS response mode has been authorised in the current user session.
4.2 Authentication step
During the authorisation session with
op.authz.alwaysPromptForAuth
enabled the Connect2id server is always going to bring up the authentication
prompt. This is necessary for the
login page to intercept the prompt=none
requests.
At the authentication prompt the
login page must check if the authorisation request has the prompt=none
and
response_mode=cors
parameters. If they are present the login page must
ensure:
-
That the Origin HTTP request header is present and it matches exactly the origin of the
redirect_uri
, e.g. for anhttps://client.example.org/cb
redirection URI the origin must equalhttps://client.example.org
. -
That the client is permitted to use the mode according to its
data.allow_response_mode_cors
metadata field.
If those criteria are met the request is allowed to proceed. If not the login
page must return an error
to the client, with invalid_request
as the recommended code and an
appropriate error_description
.
4.3 Final response step
If the CORS request was allowed to proceed, as explained above, the final
response in the authorisation
session will be a JSON object with the redirect_uri
for the client and the
authorisation parameters to return to it.
Example:
{
"uri" : "https://client.example.org/cb",
"code" : "aeL2koh8aeveishooquaeFaex7Eech7g",
"state": "Uu2ijed0"
}
The login page should take these details to produce an HTTP 200 response, setting two CORS specific headers and including the authorisation response parameters in the body.
The uri
parameter needs to be converted to an origin URL to set the
Access-Control-Allow-Origin
HTTP response header. The
Access-Control-Allow-Credentials
must be set to true
, to tell the browser
the cookie(s) were accepted.
Note, the Access-Control-Allow-Origin
value must not be set to the wildcard
(*
) when credentials are involved, in those case the allowed origin URI must
be set explicitly.
Example HTTP response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://client.example.org
Access-Control-Allow-Credentials: true
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"code" : "aeL2koh8aeveishooquaeFaex7Eech7g",
"state": "Uu2ijed0"
}
5. The OAuth client / OpenID relying party
To make a prompt=none
authorisation request
with this custom CORS response mode the client simply needs to add the extra
required response_mode
parameter, to switch from the default redirection
behaviour.
https://c2id.com/login?
response_type=code
&scope=openid
&client_id=123
&state=af0ifjsldkj
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
&prompt=none
&id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlO...
&response_mode=cors
The fetch call needs to have the credentials include parameter set, to tell the browser to include the session cookie in the CORS request.
fetch(url, { credentials:"include" }).then(success, failure)
On success, if the echoed state
parameter is valid and the client is
following the authorisation code flow, the next step is to make a call to the
token endpoint to have the code exchanged for the requested
token(s).
6. Security considerations
The security of public OAuth 2.0 clients relies on delivering the authorisation code to the client’s registered redirection URI, which must match exactly.
The CORS security on the other hand is based on web origin (URI scheme, host
and port) matching. This means that web origin matching cannot differentiate
between two OAuth clients which redirect_uri
s differ only by their path
component.
To ensure sufficient proof of client identity in CORS prompt=none
requests
the client must therefore include a previous ID token in the optional
id_token_hint
parameter, to tie the request to the original front-channel
request where the client redirect_uri
was matched exactly. This is ensured by
enabling the
op.authz.requireIDTokenHintWithPromptNone
setting.
If the CORS prompt=none
requests must also cover plain OAuth 2.0
authorisation requests, where an ID token hint cannot be included, every
registered public client must have a redirection URIs with unique host (domain)
names.