diff --git a/README.md b/README.md index c26c11b..e8558b7 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,15 @@ ![anyway here's wonderwall](https://i.imgur.com/NhRLEej.png) -`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 +Wonderwall is an application that implements an _OpenID Connect_ (OIDC) relying party/client in a way that makes it +easy to plug into Kubernetes applications as a _sidecar_. + +As such, this is OIDC as a sidecar, or OaaS, or to explain the joke: + +> _Oasis - Wonderwall_ + +Wonderwall functions as a reverse proxy that should be placed in front of your application; intercepting and proxying requests. +It provides endpoints to perform logins and logouts for end users, along with session management - so that your application does not have to. ## Features @@ -12,25 +18,40 @@ Wonderwall aims to be compliant with OAuth 2.1, and supports the following: - OpenID Connect Authorization Code Flow with mandatory use of PKCE, state and nonce - Client authentication using client assertions (`private_key_jwt`) as - per [RFC 7523, Section 2.2](https://datatracker.ietf.org/doc/html/rfc7523). -- [RP-initiated logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html). -- [Front-channel logout](https://openid.net/specs/openid-connect-frontchannel-1_0.html). + per [RFC 7523, Section 2.2](https://datatracker.ietf.org/doc/html/rfc7523) +- OpenID Connect [RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) +- OpenID Connect [Front-Channel Logout](https://openid.net/specs/openid-connect-frontchannel-1_0.html) +- Encrypted sessions with XChaCha20-Poly1305, stored using Redis as the backend +- Two deployment modes: + - Standalone mode (default) for zero-trust based setups where each application has its own perimeter and client + - Single sign-on (SSO) mode for shared authentication across multiple applications on a common domain -Wonderwall functions as an optionally intercepting reverse proxy that proxies requests to a downstream host. +## Documentation -By default, it does not actually modify any proxied request if the user agent does not have a valid session with Wonderwall. +At a glance, end-user authentication using Wonderwall is fairly straightforward: -## Development +- If the user does _not_ have a valid local session with the sidecar, requests will be proxied to the upstream host as-is without modifications. +- In order to obtain a local session, the user must be redirected to the `/oauth2/login` endpoint, which will initiate the + OpenID Connect Authorization Code Flow. + - 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 user agent to the application. + - All requests that are forwarded to the upstream host will now contain an `Authorization` header with the user's `access_token` as a Bearer token, as long as the session is not expired or inactive. +- In order to log out, the user must be redirected to the `/oauth2/logout` endpoint. -### Requirements +Detailed documentation can be found in the [documentation](docs) directory: -- Go 1.20 +- [Architecture](docs/architecture.md) +- [Configuration](docs/configuration.md) +- [Endpoints](docs/endpoints.md) +- [Usage](docs/usage.md) + - [Session Management](docs/sessions.md) + +## Running Locally ### Binary -`make wonderwall` and `./bin/wonderwall` +Requires Go 1.20 -See [configuration](#configuration). +`make wonderwall` and `./bin/wonderwall` ### Docker Compose @@ -39,23 +60,6 @@ See the [docker-compose file](docker-compose.yml) for an example setup: - You need to be able to reach `host.docker.internal` to reach the identity provider mock, so make sure you have `127.0.0.1 host.docker.internal` in your `/etc/hosts` file. - By default, the setup will use the latest available pre-built image. - - If you want to will build a fresh binary from the cloned source, replace the following - -```yaml -services: - ... - wonderwall: - image: ghcr.io/nais/wonderwall:latest - ``` - -with - -```yaml -services: - ... - wonderwall: - build: . -``` Run `docker-compose up`. This starts: @@ -74,339 +78,6 @@ Try it out: 3. Visit 1. The `authorization` header should no longer be set in the upstream response. -## Overview - -The image below shows the overall architecture of an application when using Wonderwall as a sidecar: - -```mermaid -flowchart TB - accTitle: System Architecture - accDescr: The architectural diagram shows the browser sending a request into the Kubernetes container, requesting the ingress https://<app>.nav.no, requesting the service https://<app>.<namespace>, sending it to the pod, which contains the sidecar. The sidecar sends a proxy request to the app, in addition to triggering and handling the Open ID Connect Auth Code Flow to the identity provider. The identity provider is outside the Kubernetes environment. - - idp(Identity Provider) - Browser -- 1. initial request --> k8s - Browser -- 2. redirected by Wonderwall --> idp - idp -- 3. performs OpenID Connect Auth Code flow --> Browser - - subgraph k8s [Kubernetes] - direction LR - Ingress(Ingress
https://<app>.nav.no) --> Service(Service
http://<app>.<namespace>) --> Wonderwall - subgraph Pod - direction TB - Wonderwall -- 4. proxy request with access token --> Application - Application -- 5. return response --> Wonderwall - end - end -``` - -The sequence diagram below shows the default behavior of Wonderwall: - -```mermaid -sequenceDiagram - accTitle: Sequence Diagram - accDescr: The sequence diagram shows the default behaviour of the sidecar, depending on whether the user already has a session or not. If the user does have a session, the sequence is as follows: 1. The user visits a path, that requests the ingress. 2. The request is forwarded to wonderwall 3. Wonderwall checks for a session in session storage. 4. Wonderwall attaches Authorization header and proxies request and sends it to the application. 5. The application returns a response to Wonderwall. 6. Wonderwall returns the response to the user. If the user does not have a session, the sequence is as follows: 1. The user visits a path, that requests the ingress. 2. The request is forwarded to wonderwall 3. Wonderwall checks for a session in session storage. 4. Wonderwall proxies the request as-is and sends it to the application. 5. The application returns a response to Wonderwall. 6. Wonderwall returns the response to the user. - - actor User - User->>Ingress: visits /path - Ingress-->>Wonderwall: forwards request - activate Wonderwall - Wonderwall-->>Session Storage: checks for session - alt has session - Session Storage-->>Wonderwall: session found - activate Wonderwall - Wonderwall-->>Application: attaches Authorization header and proxies request - Application-->>Wonderwall: returns response - Wonderwall->>User: returns response - deactivate Wonderwall - else does not have session - Session Storage-->>Wonderwall: no session found - activate Wonderwall - Wonderwall-->>Application: proxies request as-is - Application-->>Wonderwall: returns response - Wonderwall->>User: returns response - deactivate Wonderwall - 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 - -Wonderwall exposes and owns these endpoints (which means they will never be proxied downstream). - -Endpoints that are available for use by applications: - -| Path | Description | -|--------------------------------|------------------------------------------------------------------------------------------------| -| `GET /oauth2/login` | Initiates the OpenID Connect Authorization Code flow | -| `GET /oauth2/logout` | Performs local logout and redirects the user to global/single-logout | -| `GET /oauth2/logout/local` | Performs local logout only | -| `GET /oauth2/session` | Returns the current user's session metadata | -| `POST /oauth2/session/refresh` | Refreshes the tokens for the user's session. Requires the `session.refresh` flag to be enabled | - -Endpoints that should be registered at and only be triggered by identity providers: - -| Path | Description | -|-----------------------------------|--------------------------------------------------------------------------------------------| -| `GET /oauth2/callback` | Handles the callback from the identity provider | -| `GET /oauth2/logout/callback` | Handles the logout callback from the identity provider | -| `GET /oauth2/logout/frontchannel` | Handles global logout request (initiated by identity provider on behalf of another client) | - -## Usage - -If the user does _not_ have a valid local session with the sidecar, the request will be proxied as-is without -modifications to the upstream host. - -In order to obtain a local session, the user must be redirected to the `/oauth2/login` endpoint, which performs the -OpenID Connect Authorization Code Flow. - -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 user agent to the application. All requests that -are forwarded to the application container will now contain an `Authorization` header with the user's `access_token` -as a Bearer token. - -Do note that cookies are set for the most specific subdomain and path (if any) defined in the `ingress` configuration -variable. - -### Configuration - -Wonderwall can be configured using either command-line flags or equivalent environment variables (i.e. `-`, `.` -> `_` -and uppercase), with `WONDERWALL_` as prefix. E.g.: - -```text -openid.client-id -> WONDERWALL_OPENID_CLIENT_ID -``` - -The following flags are available: - -```shell ---auto-login Automatically redirect all HTTP GET requests to login if the user does not have a valid session for all matching upstream paths. ---auto-login-ignore-paths strings Comma separated list of absolute paths to ignore when 'auto-login' is enabled. Supports basic wildcard matching with glob-style asterisks. Invalid patterns are ignored. ---bind-address string Listen address for public connections. (default "127.0.0.1:3000") ---cookie-prefix string Prefix for cookie names. (default "io.nais.wonderwall") ---encryption-key string Base64 encoded 256-bit cookie encryption key; must be identical in instances that share session store. ---ingress strings Comma separated list of ingresses used to access the main application. ---log-format string Log format, either 'json' or 'text'. (default "json") ---log-level string Logging verbosity level. (default "info") ---metrics-bind-address string Listen address for metrics only. (default "127.0.0.1:3001") ---openid.acr-values string Space separated string that configures the default security level (acr_values) parameter for authorization requests. ---openid.client-id string Client ID for the OpenID client. ---openid.client-jwk string JWK containing the private key for the OpenID client in string format. ---openid.post-logout-redirect-uri string URI for redirecting the user after successful logout at the Identity Provider. ---openid.provider string Provider configuration to load and use, either 'openid', 'azure', 'idporten'. (default "openid") ---openid.resource-indicator string OAuth2 resource indicator to include in authorization request for acquiring audience-restricted tokens. ---openid.scopes strings List of additional scopes (other than 'openid') that should be used during the login flow. ---openid.ui-locales string Space-separated string that configures the default UI locale (ui_locales) parameter for OAuth2 consent screen. ---openid.well-known-url string URI to the well-known OpenID Configuration metadata document. ---redis.address string Address of Redis. An empty value will use in-memory session storage. ---redis.connection-idle-timeout int Idle timeout for Redis connections, in seconds. If non-zero, the value should be less than the client timeout configured at the Redis server. A value of -1 disables timeout. Default is 30 minutes. ---redis.password string Password for Redis. ---redis.tls Whether or not to use TLS for connecting to Redis. (default true) ---redis.username string Username for Redis. ---session.inactivity Automatically expire user sessions if they have not refreshed their tokens within a given duration. ---session.inactivity-timeout duration Inactivity timeout for user sessions. (default 30m0s) ---session.max-lifetime duration Max lifetime for user sessions. (default 1h0m0s) ---session.refresh Enable refresh tokens. In standalone mode, will automatically refresh tokens if they are expired as long as the session is valid (i.e. not exceeding 'session.max-lifetime' or 'session.inactivity-timeout'). ---sso.domain string The domain that the session cookies should be set for, usually the second-level domain name (e.g. example.com). ---sso.enabled Enable single sign-on mode; one server acting as the OIDC Relying Party, and N proxies. The proxies delegate most endpoint operations to the server, and only implements a reverse proxy that reads the user's session data from the shared store. ---sso.mode string The SSO mode for this instance. Must be one of 'server' or 'proxy'. (default "server") ---sso.server-default-redirect-url string The URL that the SSO server should redirect to by default if a given redirect query parameter is invalid. ---sso.server-url string The URL used by the proxy to point to the SSO server instance. ---sso.session-cookie-name string Session cookie name. Must be the same across all SSO Servers and Proxies. ---upstream-host string Address of upstream host. (default "127.0.0.1:8080") -``` - -Boolean flags/options are by default set to `false` unless noted otherwise. - -At minimum, the following configuration must be provided: - -- `openid.client-id` -- `openid.client-jwk` -- `openid.well-known-url` -- `ingress` - -#### ID-porten - -When the `openid.provider` flag is set to `idporten`, the following environment variables are bound to the required `openid` -flags described previously: - -- `IDPORTEN_CLIENT_ID` - Client ID for the client at ID-porten. -- `IDPORTEN_CLIENT_JWK` - Private key belonging to the client in JWK format. -- `IDPORTEN_WELL_KNOWN_URL` - Well-known OpenID Configuration endpoint for ID-porten: . - -The default values for the following flags are also changed: - -| Flag | Value | -|---------------------|----------| -| `openid.acr-values` | `Level4` | -| `openid.ui-locales` | `nb` | - -#### Azure AD - -When the `openid.provider` flag is set to `azure`, the following environment variables are bound to the required flags -described previously: - -- `AZURE_APP_CLIENT_ID` - Client ID for the client at Azure AD. -- `AZURE_APP_CLIENT_JWK` - Private key belonging to the client in JWK format. -- `AZURE_APP_WELL_KNOWN_URL` - Well-known OpenID Configuration endpoint for Azure AD. - -## Session Management - -Sessions are stored server-side; we only store a session identifier at the end-user's user agent. -For production use, we strongly recommend setting up and connecting to Redis. - -Sessions can be configured with a maximum lifetime with the `session.max-lifetime` flag, which accepts Go duration strings -(e.g. `10h`, `5m`, `30s`, etc.). - -There's also an endpoint that returns metadata about the user's session as a JSON object at `GET /oauth2/session`. This -endpoint will respond with HTTP status codes on errors: - -- `401 Unauthorized` - no session cookie or matching session found, or maximum lifetime reached -- `500 Internal Server Error` - the session store is unavailable, or 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 return `HTTP 200 OK` for [_inactive_ sessions](#inactivity). This allows applications 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. - -#### Example - -Request: - -``` -GET /oauth2/session -``` - -Response: - -``` -HTTP/2 200 OK -Content-Type: application/json -``` - -```json -{ - "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 - } -} -``` - -Most of these fields should be self-explanatory, but we'll be explicit with their description: - -| Field | Description | -|------------------------------|----------------------------------------------------------------------------------------------------------------------| -| `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. | -| `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.ends_in_seconds` | The number of seconds until the session ends. | -| `session.active` | Whether or not the session is marked as active. | -| `session.timeout_in_seconds` | The number of seconds until the session times out. A value of `-1` means no timeout. | -| `tokens.expire_at` | The timestamp that denotes when the tokens within the session will expire. | -| `tokens.refreshed_at` | The timestamp that denotes when the tokens within the session was last refreshed. | -| `tokens.expire_in_seconds` | The number of seconds until the tokens expire. | - -### Refresh Tokens - -Tokens within the session will usually expire before the session itself. If you've configured a longer session lifetime, -you'll probably want to use refresh tokens to avoid redirecting end-users to the `/oauth2/login` endpoint whenever the -access tokens have expired. This can be enabled by using the `session.refresh` flag. - -If enabled, tokens will be automatically renewed 5 minutes (at the earliest) before they expire. They will also be -renewed _after_ expiry, unless the session itself has ended or is marked as not active. - -Refreshing happens whenever the end-user visits any path that is proxied to the upstream application, -unless the `sso.enabled` flag is enabled. - -The `session.refresh` flag also enables a new endpoint: - -- `POST /oauth2/session/refresh` - manually refreshes the tokens for the user's session, and returns the metadata like in -`/oauth2/session` described previously - -#### Example - -Request: - -``` -POST /oauth2/session/refresh -``` - -Response: - -``` -HTTP/2 200 OK -Content-Type: application/json -``` - -```json -{ - "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 - } -} -``` - -Additionally, the metadata object returned by both the `/oauth2/session` and `/oauth2/session/refresh` endpoints now -contain some new fields in addition to the previous fields: - -| Field | Description | -|---------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `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. | - -Note that the refresh operation has a default cooldown period of 1 minute, which may be shorter depending on the token lifetime -of the tokens returned by the identity provider. In other words, a request to the `/oauth2/session/refresh` endpoint will -only trigger a refresh if `tokens.refresh_cooldown` is `false`. - -### Inactivity - -A session can be marked as inactive if the time since last refresh exceeds a given timeout. An inactive session cannot -have its tokens refreshed, and a new login is required. This is useful if you want -to ensure that an end-user can re-authenticate with the identity provider if they've been gone from an authenticated -session for some time. - -This is enabled with the `session.inactivity` option, which also requires `session.refresh`. - -The `/oauth2/session` endpoint returns `session.active`, `session.timeout_at` and `session.timeout_in_seconds` that -indicates the state of the session and when it times out. - -The timeout is configured with `session.inactivity-timeout`. -If this timeout is shorter than the token expiry, the `tokens.expire_at` and `tokens.expire_in_seconds` fields will -be reduced accordingly to reflect the inactivity timeout. - ## Verifying the Wonderwall image and its contents The image is signed "keylessly" using [Sigstore cosign](https://github.com/sigstore/cosign). diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..5969413 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,7 @@ +# Table of Contents + +- [Architecture](architecture.md) +- [Configuration](configuration.md) +- [Endpoints](endpoints.md) +- [Usage](usage.md) + - [Session Management](sessions.md) diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..76efab9 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,166 @@ +# Architecture + +The diagram below shows the overall architecture of an application when using Wonderwall as a sidecar in Kubernetes: + +```mermaid +flowchart TB + accTitle: System Architecture + accDescr: The architectural diagram shows the browser sending a request into the Kubernetes container, requesting the ingress https://<app>.nav.no, requesting the service https://<app>.<namespace>, sending it to the pod, which contains the sidecar. The sidecar sends a proxy request to the app, in addition to triggering and handling the Open ID Connect Auth Code Flow to the identity provider. The identity provider is outside the Kubernetes environment. + + idp(Identity Provider) + Browser -- 1. initial request --> k8s + Browser -- 2. redirected by Wonderwall --> idp + idp -- 3. performs OpenID Connect Auth Code flow --> Browser + + subgraph k8s [Kubernetes] + direction LR + Ingress(Ingress
https://<app>.nav.no) --> Service(Service
http://<app>.<namespace>) --> Wonderwall + subgraph Pod + direction TB + Wonderwall -- 4. proxy request with access token --> Application + Application -- 5. return response --> Wonderwall + end + end +``` + +Note that we do not provide any mechanisms to configure `Services` or inject the sidecar into `Deployments` at this time; this is left as an exercise for the reader. + +The sequence diagram below shows the default behavior of Wonderwall: + +```mermaid +sequenceDiagram + accTitle: Sequence Diagram + accDescr: The sequence diagram shows the default behaviour of the sidecar, depending on whether the user already has a session or not. If the user does have a session, the sequence is as follows: 1. The user visits a path, that requests the ingress. 2. The request is forwarded to wonderwall 3. Wonderwall checks for a session in session storage. 4. Wonderwall attaches Authorization header and proxies request and sends it to the application. 5. The application returns a response to Wonderwall. 6. Wonderwall returns the response to the user. If the user does not have a session, the sequence is as follows: 1. The user visits a path, that requests the ingress. 2. The request is forwarded to wonderwall 3. Wonderwall checks for a session in session storage. 4. Wonderwall proxies the request as-is and sends it to the application. 5. The application returns a response to Wonderwall. 6. Wonderwall returns the response to the user. + + actor User + User->>Ingress: visits /path + Ingress-->>Wonderwall: forwards request + activate Wonderwall + Wonderwall-->>Session Storage: checks for session + alt has session + Session Storage-->>Wonderwall: session found + activate Wonderwall + Wonderwall-->>Application: attaches Authorization header and proxies request + Application-->>Wonderwall: returns response + Wonderwall->>User: returns response + deactivate Wonderwall + else does not have session + Session Storage-->>Wonderwall: no session found + activate Wonderwall + Wonderwall-->>Application: proxies request as-is + Application-->>Wonderwall: returns response + Wonderwall->>User: returns response + deactivate Wonderwall + end +``` + +# Modes + +Wonderwall has two runtime modes, the choice of which depends on your specific setup: + +1. The _Standalone mode_ is the default mode and is the most restrictive. +2. The _Single Sign-On (SSO) mode_ is an optional mode where the restrictions are loosened. + +## Standalone Mode (Default) + +The standalone mode is the default mode for Wonderwall and the most restrictive mode. +It encourages a 1-to-1 mapping for a single identity provider client to a upstream application, where each application has their own identity provider client (i.e. their own set of credentials, their own set of redirect URLs, etc.) + +This mode is suitable for organizations seeking to implement zero-trust based token architectures, but requires some maturity in terms of automated provisioning and configuration of identity provider clients. + +Restrictions: + +- Cookies are set to the match the most specific domain and path (if any) for the configured `ingress`. +- Allowed redirects are also similarly restricted to the same domain and path. +- Users will have separate sessions for each application. + - If using an identity provider with SSO capabilities, this means that the user will see a "redirect blip" when navigating between applications. This may be undesirable in terms of user experience, which is an unfortunate trade-off for increased security. + - If you want sessions to be seamlessly shared between applications on a common domain, use Wonderwall in [SSO mode](#single-sign-on-sso-mode). + +Generally speaking, the recommended approach when using Wonderwall in standalone mode is to put it in front of your backend-for-frontend server that serves your frontend. +Requests to other APIs should be done through the backend-for-frontend by reverse-proxying. +This avoids having to configure CORS as well as the restrictions on cookies and allowed redirects mandated by Wonderwall. + +See the [configuration](configuration.md#standalone-mode-default) document for configuring the standalone mode. + +## Single Sign-On (SSO) Mode + +The single sign-on mode is an optional mode where some restrictions are loosened, compared to the standalone mode. + +The most notable changes are: + +- Session cookies are now set and accessible for the whole SSO (sub-)domain that is configured. +- The [`/oauth2/session`](endpoints.md#oauth2session) and [`/oauth2/session/refresh`](endpoints.md#oauth2sessionrefresh) endpoints are configured to allow CORS from origins matching the SSO (sub-)domain. +- [Automatic token refreshes are unavailable](sessions.md#automatic-vs-manual-refresh). + +The SSO mode is mostly just the standalone mode split into two parts; a server part and a proxy part. + +It allows a single identity provider client to be used across multiple upstream applications within the same domain. +While you technically can do the same using the standalone mode, that approach has multiple issues: + +- Having to distribute and synchronize the private JWK to all deployments. +- Having to manage and register each relying party's callback URL at the identity provider. Some providers also impose a limit for each client. + +Using the SSO mode only requires you to register the callback URLs that belong to the SSO server. +The server is also the only part that needs to access the private JWK; the SSO proxies will work without it. + +The diagram below shows the overall architecture when deploying Wonderwall in SSO mode: + +```mermaid +flowchart BT + accTitle: System Architecture (SSO Mode) + accDescr: The architectural diagram shows the browser sending a request into the Kubernetes container, requesting the ingress https://<app>.nav.no, requesting the service https://<app>.<namespace>, sending it to the pod, which contains the sidecar. The sidecar sends a proxy request to the app, in addition to triggering and handling the Open ID Connect Auth Code Flow to the identity provider. The identity provider is outside the Kubernetes environment. + + Browser["Browser (User Agent)"] + idp["Identity Provider"] + + Ingress["Application Ingress
https://<app>.nav.no"] + Service["Application Service
http://<app>.<namespace>"] + Wonderwall["Wonderwall SSO Proxy"] + + IngressSSO["Ingress
https://sso.nav.no"] + ServiceSSO["Service
http://wonderwall-sso-server.<namespace>"] + WonderwallSSO["Wonderwall SSO Server"] + + Browser -- 1. initial request --> Application + Application -- 2. redirected by Wonderwall SSO Proxy --> ApplicationSSO + ApplicationSSO -- 3. redirected by Wonderwall SSO Server --> idp + idp -- 4. performs OpenID Connect Auth Code flow <--> Browser + idp -- 5. redirect to callback after successful login --> ApplicationSSO + ApplicationSSO -- 6. redirect after successful callback --> Application + Application -- 8. return response --> Browser + + subgraph ApplicationSSO["Wonderwall SSO Server"] + direction TB + IngressSSO <--> ServiceSSO <--> WonderwallSSO + end + + subgraph Application["Application with SSO Proxy"] + direction TB + Ingress <--> Service <--> Wonderwall + subgraph Pod + Wonderwall -- 7. proxy request with access token <--> ApplicationContainer["Application Container"] + end + end +``` + +See the [configuration](configuration.md#single-sign-on-sso-mode) document for enabling and configuring the SSO mode. + +### SSO Server + +The SSO server effectively has the same functionality as the standalone mode and handles the same endpoints, just without the reverse-proxying to an upstream application. + +The [`/oauth2/login`](endpoints.md#oauth2login) and the [`/oauth2/logout`](endpoints.md#oauth2logout) endpoints now accept redirect URLs matching any subdomain and path within the configured SSO (sub-)domain. + +The SSO server should be deployed separately as its own application, being a central relying party for all proxies that should share the same sessions. + +### SSO Proxy + +The SSO proxy is effectively a read-only replica version of the standalone mode, providing the reverse-proxy functionality to the upstream application. + +All OpenID Connect functionality is delegated to the SSO server by means of reverse-proxying or redirects, which is completely transparent to applications. +This also means that all endpoints are still handled as before. + +Applications may thus choose to use either the SSO server or the SSO proxy endpoints, whichever is more convenient. +Bear in mind that the SSO proxy restricts allowed redirects to only relative URLs, as opposed to the SSO server. + +The SSO proxy should be deployed as a sidecar, just like the standalone mode for Wonderwall. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..488aa75 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,215 @@ +# Configuration + +Wonderwall can be configured using either command-line flags or equivalent environment variables (i.e. `-`, `.` -> `_` +and uppercase), with `WONDERWALL_` as prefix. E.g.: + +```text +openid.client-id -> WONDERWALL_OPENID_CLIENT_ID +``` + +The following flags are available: + +| Flag | Type | Description | Default Value | +|:----------------------------------|:---------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------| +| `auto-login` | boolean | Automatically redirect all HTTP GET requests to login if the user does not have a valid session for all matching upstream paths. | | +| `auto-login-ignore-paths` | strings | Comma separated list of absolute paths to ignore when `auto-login` is enabled. Supports basic wildcard matching with glob-style asterisks. Invalid patterns are ignored. | | +| `bind-address` | string | Listen address for public connections. | `127.0.0.1:3000` | +| `cookie-prefix` | string | Prefix for cookie names. | `io.nais.wonderwall` | +| `encryption-key` | string | Base64 encoded 256-bit cookie encryption key; must be identical in instances that share session store. | | +| `ingress` | strings | Comma separated list of ingresses used to access the main application. | | +| `log-format` | string | Log format, either `json` or `text`. | `json` | +| `log-level` | string | Logging verbosity level. | `info` | +| `metrics-bind-address` | string | Listen address for metrics only. | `127.0.0.1:3001` | +| `openid.acr-values` | string | Space separated string that configures the default security level (`acr_values`) parameter for authorization requests. | | +| `openid.client-id` | string | Client ID for the OpenID client. | | +| `openid.client-jwk` | string | JWK containing the private key for the OpenID client in string format. | | +| `openid.post-logout-redirect-uri` | string | URI for redirecting the user after successful logout at the Identity Provider. | | +| `openid.provider` | string | Provider configuration to load and use, either `openid`, `azure`, `idporten`. | `openid` | +| `openid.resource-indicator` | string | OAuth2 resource indicator to include in authorization request for acquiring audience-restricted tokens. | | +| `openid.scopes` | strings | List of additional scopes (other than `openid`) that should be used during the login flow. | | +| `openid.ui-locales` | string | Space-separated string that configures the default UI locale (`ui_locales`) parameter for OAuth2 consent screen. | | +| `openid.well-known-url` | string | URI to the well-known OpenID Configuration metadata document. | | +| `redis.address` | string | Address of Redis. An empty value will use in-memory session storage. | | +| `redis.connection-idle-timeout` | int | Idle timeout for Redis connections, in seconds. If non-zero, the value should be less than the client timeout configured at the Redis server. A value of `-1` disables timeout. Default is 30 minutes. | `1800` | +| `redis.password` | string | Password for Redis. | | +| `redis.tls` | boolean | Whether or not to use TLS for connecting to Redis. | `true` | +| `redis.username` | string | Username for Redis. | | +| `session.inactivity` | boolean | Automatically expire user sessions if they have not refreshed their tokens within a given duration. | | +| `session.inactivity-timeout` | duration | Inactivity timeout for user sessions. | `30m` | +| `session.max-lifetime` | duration | Max lifetime for user sessions. | `1h` | +| `session.refresh` | boolean | Enable refresh tokens. In standalone mode, will automatically refresh tokens if they are expired as long as the session is valid (i.e. not exceeding `session.max-lifetime` or `session.inactivity-timeout`). | | +| `sso.domain` | string | The domain that the session cookies should be set for, usually the second-level domain name (e.g. `example.com`). | | +| `sso.enabled` | boolean | Enable single sign-on mode; one server acting as the OIDC Relying Party, and N proxies. The proxies delegate most endpoint operations to the server, and only implements a reverse proxy that reads the user's session data from the shared store. | | +| `sso.mode` | string | The SSO mode for this instance. Must be one of `server` or `proxy`. | `server` | +| `sso.server-default-redirect-url` | string | The URL that the SSO server should redirect to by default if a given redirect query parameter is invalid. | | +| `sso.server-url` | string | The URL used by the proxy to point to the SSO server instance. | | +| `sso.session-cookie-name` | string | Session cookie name. Must be the same across all SSO Servers and Proxies that should share sessions. | | +| `upstream-host` | string | Address of upstream host. | `127.0.0.1:8080` | + +Boolean flags are by default set to `false` unless noted otherwise. + +String/strings flags are by default empty unless noted otherwise. + +Duration flags support [Go duration strings](https://pkg.go.dev/time#ParseDuration), e.g.`10h`, `5m`, `30s`, etc. + +## Production Use + +The `bind-address` configuration should be set to listen to a public interface, e.g. `0.0.0.0:3000`. +The default value only listens to the loopback interface, i.e. localhost - which makes it unavailable for services outside the Kubernetes Pod. + +The `encryption-key` configuration should be set. +Otherwise, a random key will be generated and used - which will not persist between restarts. Sessions will also be rendered invalid as they're unable to be decrypted. + +The `redis.address` configuration should be set. Otherwise, an in-memory store is used. +This is especially important if you're running multiple replicas of your application that should share the same sessions. + +## Modes + +Wonderwall has two runtime modes, a standalone mode and a single sign-on (SSO) mode. +See the [architecture](architecture.md#modes) document for further details. + +### Standalone Mode (Default) + +The default configuration of Wonderwall will start in [_standalone mode_](architecture.md#standalone-mode-default). + +At minimum, the following configuration must be provided when in standalone mode: + +- `openid.client-id` +- `openid.client-jwk` +- `openid.well-known-url` +- `ingress` + +### Single Sign-On (SSO) Mode + +When the `sso.enabled` flag is enabled, Wonderwall will start in [_SSO mode_](architecture.md#single-sign-on-sso-mode). + +There are two possible modes when in SSO mode. This is controlled with the `sso.mode` flag; the default value is `server`. + +#### SSO Server + +When the `sso.enabled` flag is enabled and the `sso.mode` flag is set to `server`, Wonderwall will start in [SSO server mode](architecture.md#sso-server). + +At minimum, the following configuration must be provided when in SSO server mode: + +- `openid.client-id` +- `openid.client-jwk` +- `openid.well-known-url` +- `ingress` +- `redis.address` +- `sso.domain` +- `sso.session-cookie-name` +- `sso.server-default-redirect-url` + +### SSO Proxy + +When the `sso.enabled` flag is enabled and the `sso.mode` flag is set to `proxy`, Wonderwall will start in [SSO proxy mode](architecture.md#sso-proxy). + +At minimum, the following configuration must be provided when in SSO proxy mode: + +- `ingress` +- `redis.address` +- `sso.session-cookie-name` +- `sso.server-url` + +## Configuration Flag Details + +--- + +### `auto-login-ignore-paths` + +List of paths or patterns to ignore when `auto-login` is enabled. + +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` +- `/a/*/*` + - ✅ matches: + - `/a/b/c` + - `/a/bee/cee` + - ❌ does not match: + - `/a` + - `/a/b` + - `/a/b/c/d` +- `/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` + +
+ +--- + +### `openid.provider` + +#### ID-porten + +When the `openid.provider` flag is set to `idporten`, the following environment variables are bound to the required `openid` +flags described previously: + +- `IDPORTEN_CLIENT_ID` + Client ID for the client at ID-porten. +- `IDPORTEN_CLIENT_JWK` + Private key belonging to the client in JWK format. +- `IDPORTEN_WELL_KNOWN_URL` + Well-known OpenID Configuration endpoint for ID-porten: . + +The default values for the following flags are also changed: + +| Flag | Value | +|---------------------|----------| +| `openid.acr-values` | `Level4` | +| `openid.ui-locales` | `nb` | + +#### Azure AD + +When the `openid.provider` flag is set to `azure`, the following environment variables are bound to the required flags +described previously: + +- `AZURE_APP_CLIENT_ID` + Client ID for the client at Azure AD. +- `AZURE_APP_CLIENT_JWK` + Private key belonging to the client in JWK format. +- `AZURE_APP_WELL_KNOWN_URL` + Well-known OpenID Configuration endpoint for Azure AD. diff --git a/docs/endpoints.md b/docs/endpoints.md new file mode 100644 index 0000000..c8c377f --- /dev/null +++ b/docs/endpoints.md @@ -0,0 +1,233 @@ +# Endpoints + +Wonderwall exposes and owns these endpoints (which means they will never be proxied to the upstream application). + +## Endpoints for applications + +Endpoints that are available for use by applications: + +| Path | Description | Notes | +|--------------------------------|----------------------------------------------------------------------|---------------------------------------------------| +| `GET /oauth2/login` | Initiates the OpenID Connect Authorization Code flow | | +| `GET /oauth2/logout` | Performs local logout and redirects the user to global/single-logout | | +| `GET /oauth2/logout/local` | Performs local logout only | Disabled when `openid.provider` is `idporten`. | +| `GET /oauth2/session` | Returns the current user's session metadata | | +| `POST /oauth2/session/refresh` | Refreshes the tokens for the user's session. | Requires the `session.refresh` flag to be enabled | + +## Endpoints for Identity Providers + +Endpoints that should be registered at and only be triggered by identity providers: + +| Path | Description | +|-----------------------------------|--------------------------------------------------------------------------------------------| +| `GET /oauth2/callback` | Handles the callback from the identity provider | +| `GET /oauth2/logout/callback` | Handles the logout callback from the identity provider | +| `GET /oauth2/logout/frontchannel` | Handles global logout request (initiated by identity provider on behalf of another client) | + +## Endpoint Details + +The `/oauth2/login` and `/oauth2/logout` endpoints respond with HTTP 3xx status codes, as these OpenID Connect flows inherently rely on browser redirects. +As such, the use of these endpoints require that user agents are _redirected_. Using the Fetch API or XHR with these endpoints will fail. + +The `/oauth2/login` and `/oauth2/logout` endpoints also accept query parameters at runtime that can override configured defaults. +These can be used to control redirect URLs and some OpenID Connect request parameters, if supported by the identity +provider. As always, query parameter string values should be URL-encoded. + +--- + +### `/oauth2/login` + +Redirect the user here to initiate the OpenID Connect Authorization Code flow. + +| Query Parameter | Description | Notes | +|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `redirect` | Where the user will be redirected after successful callback from the Identity Provider. Must be a relative URL with an absolute path, or an absolute URL. | For standalone or SSO proxy mode, this effectively only allows relative URLs. For SSO server mode, the domain must match any subdomain and path within the configured SSO domain. | +| `level` | The `acr_values` parameter for the OpenID Connect authentication request. | Value must be declared as supported by the Identity Provider through the `acr_values_supported` property in the metadata document. | +| `locale` | The `ui_locales` parameter for the OpenID Connect authentication request | Value must be declared as supported by the Identity Provider through the `ui_locales_supported` property in the metadata document. | + +The user will be sent to the identity provider for authentication, and then back to the `/oauth2/callback` endpoint. + +Following this, the user will be redirected using the following priority: + +1. To the URL provided in the `redirect` query parameter in the initial login-request. +2. If the query parameter was not set or invalid, the redirect will point to different places depending on the [runtime mode](configuration.md#modes): + - Standalone: the context root for the matching ingress that received the HTTP request. + - SSO: the default URL configured using the `sso.server-default-redirect-url` flag. + +--- + +### `/oauth2/logout` + +Redirect the user here to clear the session along with local cookies, and to initiate the OpenID Connect RP-Initiated Logout flow. + +| Query Parameter | Description | Notes | +|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `redirect` | Where the user will be redirected after successful callback from the Identity Provider. Must be a relative URL with an absolute path, or an absolute URL. | For standalone or SSO proxy mode, this effectively only allows relative URLs. For SSO server mode, the domain must match any subdomain and path within the configured SSO domain. | + +The user will be sent to the identity provider for logout, and then back to the `/oauth2/logout/callback` endpoint. + +Following this, the user will be redirected using the following priority: + +1. To the URL provided in the `redirect` query parameter in the initial logout-request. +2. If the query parameter was not set or invalid, the URL in the `openid.post-logout-redirect-uri` will be used. +3. If the `openid.post-logout-redirect-uri` flag is not set or empty, to the context root for the matching ingress that received the HTTP request. + +--- + +### `/oauth2/logout/local` + +Perform a `GET` request from the user agent (e.g. using the Fetch API or XHR) to this endpoint to clear the session along with local cookies. + +**This does _not_ perform single sign-out at the identity provider; use the `/oauth2/logout` endpoint instead if you intend to log a user out globally.** + +This endpoint only responds with a HTTP 204 No Content on successful local logout. + +It may respond with a HTTP 500 if the session could not be cleared. + +--- + +### `/oauth2/session` + +Perform a `GET` request from the user agent to receive metadata about the user's session as a JSON object. + +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 reached +- `HTTP 500 Internal Server Error` - the session store is unavailable, or 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](sessions.md#session-inactivity), as long as the session is not [_expired_](sessions.md#session-expiry). +This allows applications 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. + +#### Request: + +``` +GET /oauth2/session +``` + +#### Response: + +``` +HTTP/2 200 OK +Content-Type: application/json +``` + +```json +{ + "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 + } +} +``` + +| 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. | + +If the `session.refresh` flag is enabled, the metadata response will contain a few additional fields: + +#### Request: + +``` +GET /oauth2/session +``` + +#### Response: + +``` +HTTP/2 200 OK +Content-Type: application/json +``` + +```json +{ + "session": { + ... + }, + "tokens": { + ... + "next_auto_refresh_in_seconds": -1, + "refresh_cooldown": false, + "refresh_cooldown_seconds": 0 + } +} +``` + +(fields shown earlier are omitted from this example for brevity) + +| Field | Description | +|---------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `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. | + +--- + +### `/oauth2/session/refresh` + +This endpoint only exists if the `session.refresh` flag is enabled. + +Perform a `POST` request from the user agent to this endpoint to manually refresh the tokens for the user's session. + +The endpoint will respond with a `HTTP 401 Unauthorized` if the session is [_inactive_](sessions.md#session-inactivity). +It is otherwise equivalent to [the `/oauth2/session` endpoint](#oauth2session) described previously. + +#### Request: + +``` +POST /oauth2/session/refresh +``` + +#### Response: + +``` +HTTP/2 200 OK +Content-Type: application/json +``` + +```json +{ + "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 default _cooldown_ period of 1 minute, which may be shorter depending on the token lifetime +of the tokens returned by the identity provider. +The cooldown period exists to limit the amount of refresh token requests that we send to the identity provider. + +A refresh is only triggered if `tokens.refresh_cooldown` is `false`. Requests to the endpoint are idempotent while the cooldown is active. diff --git a/docs/sessions.md b/docs/sessions.md new file mode 100644 index 0000000..cb17bfb --- /dev/null +++ b/docs/sessions.md @@ -0,0 +1,54 @@ +# Session Management + +Sessions are stored server-side; we only store a session identifier at the end-user's user agent. + +## Session Metadata + +User agents can access their own session metadata by using [the `/oauth2/session` endpoint](endpoints.md#oauth2session). + +## Session Expiry + +Every session has a maximum lifetime. +The lifetime is indicated by the `session.ends_at` and `session.ends_in_seconds` fields in the session metadata. + +When the session reaches the maximum lifetime, it is considered to be _expired_ or _ended_, after which the user is essentially unauthenticated. +A new session must be acquired by redirecting the user to [the `/oauth2/login` endpoint](endpoints.md#oauth2login) again. + +The maximum lifetime can be configured with the `session.max-lifetime` flag. + +## Session Refreshing + +The tokens within the session will usually expire before the session itself. +This is indicated by the `tokens.expire_at` and `tokens.expire_in_seconds` fields in the session metadata. + +If you've configured a session lifetime that is longer than the token expiry, you'll probably want to _refresh_ the tokens to avoid redirecting end-users to the `/oauth2/login` endpoint whenever the access tokens have expired. + +The ability to refresh tokens requires the `session.refresh` flag to be enabled. + +### Automatic vs Manual Refresh + +The behaviour for refreshing depends on the [runtime mode](configuration.md#modes) for Wonderwall. + +In standalone mode, tokens will at the _earliest_ automatically be renewed 5 minutes before they expire. +If the token already _has_ expired, a refresh attempt is still automatically triggered as long as the session itself not has ended or is marked as inactive. + +Automatic refreshes happens whenever the end-user visits or requests any path that is proxied to the upstream application. + +In SSO mode, tokens are not automatically refreshed, and must be manually refreshed by performing a request to [the `/oauth2/session/refresh` endpoint](endpoints.md#oauth2sessionrefresh). + +## Session Inactivity + +A session can be marked as _inactive_ before it _expires_ (reaches the maximum lifetime). +This happens if the time since the last _refresh_ exceeds the given _inactivity timeout_. + +An inactive session _cannot_ be refreshed; a new session must be acquired by redirecting the user to the `/oauth2/login` endpoint. +This is useful if you want to ensure that an end-user can re-authenticate with the identity provider if they've been gone from an authenticated session for some time. + +Inactivity support is enabled with the `session.inactivity` option, which also requires `session.refresh`. + +The activity state of the session is indicated by the `session.active` field in the session metadata. + +The time until the session will be marked as inactive are indicated by the `session.timeout_at` and `session.timeout_in_seconds` fields in the session metadata. + +The timeout is configured with `session.inactivity-timeout`. +If this timeout is shorter than the token expiry, the session metadata fields `tokens.expire_at` and `tokens.expire_in_seconds` will be reduced accordingly to reflect the inactivity timeout. diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..b2b861a --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,56 @@ +# Usage + +The contract for using Wonderwall is fairly straightforward. + +For any endpoint that requires authentication: + +1. Validate the `Authorization` header, and any tokens within. +2. If the `Authorization` header is missing, redirect the user to the [login endpoint](#1-login). +3. If the JWT `access_token` in the `Authorization` header is invalid or expired, redirect the user to + the [login endpoint](#1-login). +4. If you need to log out a user, redirect the user to the [logout endpoint](#2-logout). + +Note that Wonderwall does not validate the `access_token` that is attached; this is the responsibility of the upstream application. +Wonderwall only validates the `id_token` in accordance with the OpenID Connect Core specifications. + +## Scenarios + +### 1. Login + +When you must authenticate a user, redirect to the user to [the `/oauth2/login` endpoint](endpoints.md#oauth2login). + +#### 1.1. Autologin + +The `auto-login` option (disabled by default) will configure Wonderwall to automatically redirect any HTTP `GET` requests to the login endpoint if the user does not have a valid session. +It will automatically set the `redirect` parameter for logins to the URL for the original request so that the user is redirected back to their intended location after login. + +You should still check the `Authorization` header for a token and validate the token. +This is especially important as auto-login will **NOT** trigger for HTTP requests that are not `GET` requests, such as `POST` or `PUT`. + +To ensure smooth end-user experiences whenever their session expires, your application must thus actively validate and +properly handle such requests. For example, your application might respond with an HTTP 401 to allow frontends to +cache or store payloads before redirecting them back to the login endpoint. + +### 2. Logout + +When you must authenticate a user, redirect to the user to [the `/oauth2/logout` endpoint](endpoints.md#oauth2logout). + +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. + +#### 2.1 Local Logout + +If you only want to perform a _local logout_ for the user, perform a `GET` request from the user's browser / user agent to [the `/oauth2/logout/local` endpoint](endpoints.md#oauth2logoutlocal). + +This will only clear the user's local session (i.e. remove the cookies) with the sidecar, without performing global logout at the identity provider. +The endpoint responds with a HTTP 204 after successful logout. It will **not** respond with a redirect. + +A local logout is useful for scenarios where users frequently switch between multiple accounts. +This means that they do not have to re-enter their credentials (e.g. username, password, 2FA) between each local logout, as they still have an SSO-session logged in with the identity provider. +If the user is using a shared device with other users, only performing a local logout is thus a security risk. + +**Ensure you understand the difference in intentions between the two logout endpoints. If you're unsure, use `/oauth2/logout`.** + +### 3. Advanced: Session Management + +See the [session management](sessions.md) page for details.