Skip to content

Single Page Applications

Hans Zandbelt edited this page May 26, 2023 · 22 revisions

Implicit Client Profile

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.

Session Info

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 unless (since 2.4.14.2rc0) extend_session=false is passed as a query parameter.

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.

Access Token Refresh

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.

Refresh Access Token Ahead of Expiry

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)
# The optional logout_on_error flag makes the refresh logout the current local session if the refresh fails.
#OIDCRefreshAccessTokenBeforeExpiry <seconds> [logout_on_error]

See also: https://github.com/zmartzone/mod_auth_openidc/wiki/Sessions-and-Timeouts#single-page-applications

UserInfo Refresh

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.

Allowing both OAuth 2.0 and OpenID Connect

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!

Advanced

Session Info Authorization

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>

Avoid state cookie overload

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