Wonderwall (sidecar for authentication)¶
Wonderwall is an application that implements an OpenID Connect (OIDC) Relying Party (client) in a way that makes it easy to plug into Kubernetes as a sidecar.
As such, this is OIDC as a sidecar, or OaaS, or to explain the joke:
Oasis - Wonderwall
Availability
Wonderwall is only available in the Google Cloud Platform clusters.
Overview¶
Wonderwall is a reverse-proxy that sits in front of your application. All incoming requests to your application are intercepted and proxied by the sidecar.
If the user does not have a valid session with the sidecar, requests will be proxied as-is without modifications to the application:
graph LR
style Wonderwall stroke:#0f0,stroke-dasharray: 5
style Application stroke:#f00
U((User)) -- "request" ---> Wonderwall
Wonderwall -. "proxy request\n as-is" -..-> Application
In order to obtain a local session, the user must be redirected to the /oauth2/login
endpoint.
This will initiate the OpenID Connect Authorization Code Flow:
graph LR
IDP[Identity Provider]
style Wonderwall stroke:#0f0,stroke-dasharray: 5
style IDP stroke:#f00
U((User)) -- "/oauth2/login" --> Wonderwall
subgraph OIDC["OpenID Connect Authorization Code Flow"]
IDP -- "redirect callback" ---> Wonderwall
Wonderwall -- "redirect to log in" ---> IDP
end
If the user successfully completed the login flow, the sidecar creates and stores a session. A corresponding session cookie is created and set before finally redirecting the user agent back to the application:
graph LR
style Wonderwall stroke:#0f0,stroke-dasharray: 5
style IDP stroke:#f00
IDP[Identity Provider] -- "redirect after login" --> Wonderwall
subgraph Wonderwall
Server -- "manage sessions" --> Store[Session Store]
end
Wonderwall -- "establish session" --> U((User))
All authenticated requests that are forwarded to the application will now contain the user's access_token
.
The token is sent as a Bearer token in the Authorization
header.
This applies as long as the session is not expired or inactive:
graph LR
style Wonderwall stroke:#0f0,stroke-dasharray: 5
style Application stroke:#f00
subgraph Session["Authenticated Session"]
direction LR
U((User)) -- "request" ---> Wonderwall
Wonderwall -. "proxy request\n with token" -..-> Application
end
Generally speaking, the recommended approach when using the Wonderwall sidecar is to put it in front of your backend-for-frontend server that serves your frontend. Otherwise, you might run into issues with the cookie configuration and allowed redirects - these are both effectively restricted to only match the domain and path for your application's ingress.
Endpoints¶
The sidecar provides these endpoints under your application's ingress:
Path | Description | Details |
---|---|---|
GET /oauth2/login |
Initiates the OpenID Connect Authorization Code flow | 1. Initiate Login |
GET /oauth2/logout |
Performs local logout and redirects the user to global/single-logout | 2. Initiate Logout |
GET /oauth2/session |
Returns the current user's session metadata | 5.1 Session Metadata |
POST /oauth2/session/refresh |
Refreshes the tokens and returns the session metadata for the current user | 5.2 Session Refresh |
Identity Providers¶
Wonderwall currently supports the following identity providers:
-
ID-porten
For citizen end-users.
-
Azure AD
For internal end-users or employees.
Minimal configuration examples below:
Usage¶
The contract for usage of the sidecar is fairly straightforward.
For any endpoint that requires authentication:
- Validate the
Authorization
header. Your application is responsible for doing this validation. - If the
Authorization
header is missing, redirect the user to the login endpoint. - If the JWT
access_token
in theAuthorization
header is invalid or expired, redirect the user to the login endpoint. - If you need to log out a user, redirect the user to the logout endpoint.
1. Initiate Login¶
When you must authenticate a user, redirect to the user to:
The user will be sent to the identity provider for authentication and then back to the sidecar's callback endpoint.
1.1. Redirect after Login¶
After the callback is handled and the user is successfully authenticated, the user will by default be redirected to the matching root context path for your application's ingress, e.g:
/
forhttps://<app>.nav.no
, or/path
forhttps://nav.no/path
To override this, use the redirect
parameter to specify an URL or absolute path:
The host and scheme (if provided) are stripped from the redirect URL, which effectively only allows redirects to paths within your own ingress.
1.2. Autologin¶
Autologin vs. Token Validation
Autologin does not perform nor is it a replacement for token validation.
Always validate the token for any endpoint that requires authentication.
Validation is especially important for requests that access sensitive data or otherwise performs operations that modify state.
The autoLogin
option will configure Wonderwall to enforce authentication for all requests, except for the paths that are explicitly excluded.
Autologin must be explicitly enabled. It is not enabled by default.
Example configuration:
If the user is unauthenticated or has an inactive or expired session, all requests will be short-circuited (i.e. return early and not proxied to your application).
The short-circuited response depends on whether the request is a top-level navigation request or not.
What is a top-level navigation request?
A top-level navigation request is a GET
request that fulfills at least one of the following properties:
- Has the Fetch metadata request headers
Sec-Fetch-Dest=document
andSec-Fetch-Mode=navigate
, or - Has an
Accept
header that includestext/html
All major modern browsers sends at least one of these for navigational requests, with Internet Explorer 8 being the only known exception. Hopefully you're not in a position that requires supporting that browser.
A top-level navigation request results in a HTTP 302 Found
response with the Location
header pointing to the login endpoint.
In order to preserve the user's original location, the redirect
parameter is set to the original request's Referer
header.
If the Referer
header is empty, we use the matching ingress context path for the original request.
All other requests (such as POST
or PUT
requests, or fetch
, XMLHttpRequest
/XHR
or AJAX
from browsers) are not considered navigational requests.
These requests result in a HTTP 401 Unauthorized
response with the Location
header set as described above.
Ensure that your frontend handles the HTTP 401
response and redirects the user to the login endpoint.
1.3. Autologin Exclusions¶
Autologin will by default match all paths for your application's ingresses, except the following:
/oauth2/*
spec.prometheus.path
, if definedspec.liveness.path
, if definedspec.readiness.path
, if defined
You can define additional paths or patterns to be excluded:
The paths must be absolute paths. The match patterns use glob-style matching.
Example match patterns (click to expand)
/allowed
or/allowed/
- Trailing slashes in paths and patterns are effectively ignored during matching.
- ✅ matches:
/allowed
/allowed/
- ❌ does not match:
/allowed/nope
/allowed/nope/
/public/*
- A single asterisk after a path means any subpath directly below the path, excluding itself and any nested paths.
- ✅ matches:
/public/a
- ❌ does not match:
/public
/public/a/b
/public/**
- Double asterisks means any subpath below the path, including itself and any nested paths.
- ✅ matches:
/public
/public/a
/public/a/b
- ❌ does not match:
/not/public
/not/public/a
/any*
- ✅ matches:
/any
/anything
/anywho
- ❌ does not match:
/any/thing
/anywho/mst/ve
- ✅ matches:
/a/*/*
- ✅ matches:
/a/b/c
/a/bee/cee
- ❌ does not match:
/a
/a/b
/a/b/c/d
- ✅ matches:
/static/**/*.js
- ✅ matches:
/static/bundle.js
/static/min/bundle.js
/static/vendor/min/bundle.js
- ❌ does not match:
/static
/static/some.css
/static/min
/static/min/some.css
/static/vendor/min/some.css
- ✅ matches:
2. Initiate Logout¶
When you must log a user out, redirect to the user to:
The user's session with the sidecar will be cleared, and the user will be redirected to the identity provider for global/single-logout, if logged in with SSO (single sign-on) at the identity provider. They will then be redirected back to the sidecar's callback endpoint.
2.1. Redirect after Logout¶
After the callback is handled, the user will receive a final redirect to a preconfigured URL. Depending on the provider, this will either be a common logout page or your application's ingress.
To override this, use the redirect
parameter as described in 1.1. Redirect after Login.
The same restrictions and caveats apply here.
3. Token Validation¶
The sidecar attaches an Authorization
header with the user's access_token
as a Bearer token, as long as the user has an active session:
It is your responsibility to validate the token before granting access to resources.
For any endpoint that requires authentication; deny access if the request does not contain a valid Bearer token.
See the specific identity provider pages for further details on token validation:
4. Resource Requirements¶
The sidecar container is set up with some default resource requirements. This can be customized to your needs. Defaults shown below:
5. Sessions¶
When a user authenticates themselves, they receive a session. Sessions are stored server-side; we only store a session identifier at the end-user's user agent.
A session has three possible states:
- active - the session is valid
- inactive - the session has reached the inactivity timeout and is considered invalid
- expired - the session has reached its maximum lifetime and is considered invalid
Requests with an invalid session are considered unauthenticated. In this state, the a new login must be initiated to acquire a new session.
The session's inactivity timeout is reset every time the user's session is refreshed.
The maximum session lifetime and inactivity timeout depends on the identity provider:
Maximum Lifetime | Inactivity Timeout |
---|---|
6 hours | ✅ Yes, 1 hour |
For ID-porten, nav-dekoratoren implements automatic support for user-triggered refreshing as well as displaying logout warnings. Refer to their documentation for further details.
If you're not using nav-dekoratoren
, see 5.2 Session Refresh for details.
Maximum Lifetime | Inactivity Timeout |
---|---|
10 hours | ❌ No timeout |
For Azure AD, if you have users actively working beyond the maximum session lifetime (which they generally shouldn't), consider notifying them before their session expires.
You can make use of the session metadata endpoint to track the session's state.
The maximum lifetime and inactivity timeout (if enabled) durations are subject to change. You should not hard-code or depend on these exact values in your application.
5.1. Session Metadata¶
GET /oauth2/session
is an endpoint that returns metadata about the user's session as a JSON object.
Requests to this endpoint must be triggered from the user's browser.
Session Metadata Example
Request:
Response:
{
"session": {
"created_at": "2022-08-31T06:58:38.724717899Z",
"ends_at": "2022-08-31T16:58:38.724717899Z",
"timeout_at": "0001-01-01T00:00:00Z",
"ends_in_seconds": 14658,
"active": true,
"timeout_in_seconds": -1
},
"tokens": {
"expire_at": "2022-08-31T14:03:47.318251953Z",
"refreshed_at": "2022-08-31T12:53:58.318251953Z",
"expire_in_seconds": 4166
"next_auto_refresh_in_seconds": -1,
"refresh_cooldown": false,
"refresh_cooldown_seconds": 0
}
}
The table below describes the different fields in the JSON response.
Session Metadata Field Descriptions (click to expand)
Field | Description |
---|---|
session.active |
Whether or not the session is marked as active. If false , the session cannot be extended and the user must be redirected to login. |
session.created_at |
The timestamp that denotes when the session was first created. |
session.ends_at |
The timestamp that denotes when the session will end. After this point, the session cannot be extended and the user must be redirected to login. |
session.ends_in_seconds |
The number of seconds until session.ends_at . |
session.timeout_at |
The timestamp that denotes when the session will time out. The zero-value, 0001-01-01T00:00:00Z , means no timeout. |
session.timeout_in_seconds |
The number of seconds until session.timeout_at . A value of -1 means no timeout. |
tokens.expire_at |
The timestamp that denotes when the tokens within the session will expire. |
tokens.expire_in_seconds |
The number of seconds until tokens.expire_at . |
tokens.refreshed_at |
The timestamp that denotes when the tokens within the session was last refreshed. |
tokens.next_auto_refresh_in_seconds |
The number of seconds until the earliest time where the tokens will automatically be refreshed. A value of -1 means that automatic refreshing is not enabled. |
tokens.refresh_cooldown |
A boolean indicating whether or not the refresh operation is on cooldown or not. |
tokens.refresh_cooldown_seconds |
The number of seconds until the refresh operation is no longer on cooldown. |
This endpoint will respond with the following HTTP status codes on errors:
HTTP 401 Unauthorized
- no session cookie or matching session found, or maximum lifetime reachedHTTP 500 Internal Server Error
- Wonderwall wasn't able to process the request
Otherwise, an HTTP 200 OK
is returned with the metadata with the application/json
as the Content-Type
.
Note that this endpoint will still return HTTP 200 OK
for inactive sessions, as long as the session is not expired.
This allows application to display errors before redirecting the user to login on timeouts.
This also means that you should not use the HTTP response status codes alone as an indication of whether the user is authenticated or not.
5.2. Session Refresh¶
Tokens within the session usually expire before the session itself.
To avoid redirecting end-users to the /oauth2/login
endpoint whenever the access tokens have expired, we can use refresh tokens to silently get new tokens.
An inactive or expired session cannot be refreshed.
For ID-porten, session refreshes must be triggered manually. nav-dekoratoren implements automatic support for user-triggered refreshes as well as displaying logout warnings. Refer to their documentation for further details.
If you're not using nav-dekoratoren
and wish to implement refreshes yourselves, these are the cases (with suggested solutions) you should handle in descending priority:
Cases for Manual Session Refreshes (click to expand)
-
The
/oauth2/session
endpoint returns 401; the user is either not authenticated or their previous session has ended.The user should either automatically be redirected to login, or be presented with a message stating that they are not authenticated with an option to either be redirected to log in or return to another page.
-
The session is inactive (or timed out).
The user should be presented with a message explaining that they've been inactive for too long and were logged out for security reasons. They should then be given the option to either be redirected to login or return to another page.
-
The session is active, but will end in around 5-10 minutes.
The user should be presented with a message that explains that the session is about to end (perhaps with a countdown), and be encouraged to save or complete their work. If the session ends while the user is still present, transition to case 1.
-
The session is active, but will time out in around 5-10 minutes.
The user should be presented with a message explaining that they've been inactive for a while and will be logged out within x time (perhaps with a countdown) if no action is taken.
They should be given an option to either extend the session or log out. If no action is taken and the session eventually times out, transition to case 2.
-
The session is active, and the tokens within the session have either expired or are 5-10 minutes away from expiring.
If you want all session refrehes to require explicit consent by the end-user, go to case 4.
If you want to refresh the user's session without prompting the user, read on.
First, determine whether the user is still present and active. This should be done with some heuristic specific to your frontend (e.g. using the Idle Detection API, time since last request, and so on). If you deem the user to still be present and active, it should be reasonable to just perform automatically perform the refresh in the background.
For Azure AD, tokens are automatically refreshed for all sessions until the session itself expires reaches the maximum lifetime.
The tokens will at the earliest be automatically refreshed 5 minutes before they expire. If the token already has expired for an active session, a refresh attempt is automatically triggered on the next request on any path that belongs to the application.
This means that you do not need to implement any token refreshing logic yourself.
POST /oauth2/session/refresh
is an endpoint that manually refreshes the tokens for the user's session.
Requests to this endpoint must be triggered from the user's browser.
The endpoint will respond with a HTTP 401 Unauthorized
if the session is inactive.
It is otherwise equivalent to the /oauth2/session
endpoint described previously.
Session Refresh Example
Request:
Response:
{
"session": {
"created_at": "2022-08-31T06:58:38.724717899Z",
"ends_at": "2022-08-31T16:58:38.724717899Z",
"timeout_at": "0001-01-01T00:00:00Z",
"ends_in_seconds": 14658,
"active": true,
"timeout_in_seconds": -1
},
"tokens": {
"expire_at": "2022-08-31T14:03:47.318251953Z",
"refreshed_at": "2022-08-31T12:53:58.318251953Z",
"expire_in_seconds": 4166,
"next_auto_refresh_in_seconds": 3866,
"refresh_cooldown": true,
"refresh_cooldown_seconds": 37
}
}
Note that the refresh operation has a cooldown period.
A refresh is only triggered if tokens.refresh_cooldown
is false
.
Requests to the endpoint are idempotent while the cooldown is active.
Next Steps¶
The access token that Wonderwall provides should only be accepted and used by your application.
In order to access other applications, you should exchange the token in order to get a new token that is correctly scoped to access a given application.
For ID-porten, use the token exchange grant (TokenX) to do this.
For Azure AD, use the on-behalf-of grant to do this.
Next.js Library
See https://github.com/navikt/next-auth-wonderwall for a Next.js library that provides Wonderwall integrations and token exchange utilities.
Created: 2023-10-16