-
Notifications
You must be signed in to change notification settings - Fork 327
Single Page Applications
The (now deprecated) canonical solution for using OpenID Connect with Single Page Applications (SPAs) is to use the Implicit grant type and have an access_token
and an id_token
delivered to the SPA in the hash fragment of a URL (i.e. the Redirect URI). The SPA will then use the access_token
to access resources protected by mod_auth_openidc
configured as an OAuth 2.0 Resource Server
However, the Implicit Grant type has been deprecated in recent OAuth 2.0 best practices and security guidelines.
It has a number of drawbacks (described here) which is why mod_auth_openidc
offers an alternative and arguably more secure way of handling OpenID Connect for Single Page Applications, described below. This approach makes the module serve as a secure backend for the SPA, where all OAuth 2.0 protocol handling is done in the backend and the SPA communicates with a backend using a traditional plain session cookie, without requiring any knowledge of OAuth 2.0.
Update Nov 2021: this approach started in 2017 is nowadays more universally accepted under the name "OAuth 2.0 Backend For Frontend" and expected to be published as a standardize guideline, possibly with standardized protocol interactions and endpoints.
For Single Page Applications (SPAs) and other Clients that require access to the access_token
or other information related to an authenticated user session, mod_auth_openidc has the ability to return "session info" to the caller when a valid session cookie is presented on a HTTP request to the following endpoint:
<redirect_uri>?info=json
The session info is returned as a JSON object and the information returned in that is configurable through OIDCInfoHook
:
# Define the data that will be returned upon calling the info hook.
# The data can be JSON formatted using <redirect_uri>?info=json, or HTML formatted, using <redirect_uri>?info=html.
# iat (int) : Unix timestamp indicating when this data was created
# access_token (string) : the access token
# access_token_expires (int) : the Unix timestamp which is a hint about when the access token will expire (as indicated by the OP)
# id_token (object) : the claims presented in the ID token
# userinfo (object) : the claims resolved from the UserInfo endpoint
# refresh_token (string) : the refresh token (if returned by the OP)
# exp (int) : the maximum session lifetime (Unix timestamp in seconds)
# timeout (int) : the session inactivity timeout (Unix timestamp in seconds)
# remote_user (string) : the remote user name
# session (object) : (for debugging) mod_auth_openidc specific session data such as "remote user", "session expiry", "session id" and a "state" object
# Note that when using ProxyPass / you may have to add a proxy exception for the Redirect URI
# for this to work, e.g. ProxyPass /redirect_uri !
# When not defined the session hook will not return any data but a HTTP 404
#OIDCInfoHook [iat|access_token|access_token_expires|id_token|userinfo|refresh_token|exp|timeout|remote_user|session]+
Calling this hook will also reset the session inactivity timer.
For restricting access to this information see: Session Info Authorization in the "Advanced" section below.
Caveat: Note that for this particular hook, the full request handling chain of Apache is executed instead of just the authentication/authorization hooks. That means that any directives like ProxyPass
that apply to this path would also execute for "info=json" as opposed to the other calls to OIDCRedirectURI
that would terminate early. Hence the OIDCRedirectURI
may need to be excluded from such (global) directives to allow the session info hook to complete.
The Client calling the hook can indicate requirements on the "freshness" of the access_token
by providing the parameter access_token_refresh_interval
:
<redirect_uri>?info=json&access_token_refresh_interval=<seconds>
Providing a value of "0" will refresh the current access_token
, but only when a refresh_token
was provided as part of the initial authentication flow and was stored in the session.
Providing a value greater than "0
" will not refresh the access token unless more than "<seconds>
" have elapsed since the last refresh.
Since version 2.3.10rc0 mod_auth_openidc supports autonomous access token refresh (optionally) ahead of the access token expiry time by setting OIDCRefreshAccessTokenBeforeExpiry
. This allows SPAs that (only) call into endpoints served through mod_auth_openidc, to use the session cookie on the calls only and to not care about the access token or its expiry. OIDCRefreshAccessTokenBeforeExpiry
can be used/set on a per-path basis. The (now always fresh...) access token may be used on propagated requests to backend APIs e.g. by using RequestHeader set Authorization "Bearer %{OIDC_access_token}e" env=OIDC_access_token
or using the (standard provided) OIDC_access_token
header.
# Specify the minimum time-to-live for the access token stored in the OIDC session.
# When the access token expiry timestamp (or at tleast the hint given to that) is less than this value,
# an attempt will be made to refresh the access token using the refresh token grant type with the OP.
# This only has effect if a refresh token was actually returned from the OP and an "expires_in" hint
# was returned as part of the authorization response (and subsequent refresh token responses).
# When not defined no attempt is made to refresh the access token (unless implicitly with OIDCUserInfoRefreshInterval)
#OIDCRefreshAccessTokenBeforeExpiry <seconds>
Note that the update frequency of the information returned from the UserInfo endpoint at the OpenID Connect Provider can be configured through the OIDCUserInfoRefreshInterval
configuration primitive and that accessing that endpoint may in itself lead to a refresh of the access_token
and possibly the refresh_token
.
In certain SPA use cases content may be served to browsers (as HTML) and to Javascript clients (as JSON) from the same path. In that case the mod_auth_openidc needs to be able to consume and validate both OAuth 2.0 bearer Access Tokens as well as session cookies on the same path. In Apache speak this means specifying both AuthType oauth20
and AuthType openid-connect
in the same Location
or Directory
. This is handled by a 3rd directive AuthType auth-openidc
.
Caveat: use this "mixed mode" with care as one typically wants to avoid to accept just any bearer token that was issued by your authorization server, so don't use just Require valid-user
!
A secure configuration would be:
<Location /example>
AuthType auth-openidc
Require claim client_id:xxx
Require claim aud:xxx
</Location>
Where the first Require
directive restricts the (Javascript) OAuth 2.0 Client having access to this path and the second Require
directive ensures that browsers have to present a session cookie issued by this Relying Party instance of mod_auth_openidc.
Note that just including Require valid-user
for the latter would defeat the first Require
. Hence you must identify sessions more specifically based on the claims in the id_token
or obtained from the UserInfo endpoint. In multi-provider setups you may need to add multiple Require claim aud:xxx
directives since typically different client_id
's would be used with different Providers. Alternatively you can base it on a claim that is issued by all Providers but is NOT part of Access Tokens issued to other Clients than your SPA's Javascript client.
Caveat: Also note that due to the nature of the "mixed mode" you should take care that overlapping claims in the id_token/userinfo
and the access_token
have the same semantics!
By default all Clients presenting a valid session cookie will receive the information configured in OIDCInfoHook
. Further refinement of that can be done by using Require
directives on the hook itself, i.e. the OIDCRedirectURI
, e.g. restricting it to the module's Client identifier for a specific Provider when multiple Providers are used:
<Location /redirect_uri>
AuthType openid-connect
Require claim aud:ac_oic_client
</Location>
Or a more advanced example using the Apache 2.4 "<If>" primitive to apply fine-grained authorization only when the session info hook is called:
<Location /redirect_uri>
<If "%{QUERY_STRING} =~ /.*info=json.*/" >
Require claim aud:ac_oic_client
</If>
<Else>
Require valid-user
</Else>
</Location>
When an SPA fires off an XHR requests that is unauthenticated - i.e. no session cookie is presented or the associated session has expired - the default action for the module is to send the Client off to the OpenID Connect Provider with a redirect. However, for SPAs this is pointless since such an XHR request can never authenticate to the OP on its own without user interaction (unless an SSO session exists, but that only defers the real problem until the SSO session expires!). Yet a state cookie is created for each such request, and since the caller never returns from the redirect with an authentication response, the state cookie is never cleaned up. This easily leads to an overload of state cookies and at a certain point the browser or server may refuse to process the request because of size overload.
For a detailed description of this problem and solutions, see: https://github.com/zmartzone/mod_auth_openidc/wiki/Cookies#state-cookies-are-piling-up