Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 86 additions & 19 deletions AUTHENTICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,49 @@ auth:

#### Auth Settings

| Field | Type | Description |
| -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `enabled` | boolean | Enable or disable authentication |
| `redirectToProvider` | boolean | Skip the Temporal UI login page and redirect unauthenticated users directly to the configured OIDC provider |
| `maxSessionDuration` | duration | Maximum session duration before forced re-login (e.g., `8h`, `24h`, `168h`). Set to `0` or omit for unlimited session duration |
| `providers` | array | List of auth providers (currently only the first is used) |
| Field | Type | Description |
| -------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | boolean | Enable or disable authentication |
| `redirectToProvider` | boolean | Skip the Temporal UI login page and redirect unauthenticated users directly to the configured OIDC provider |
| `maxSessionDuration` | duration | Maximum session duration before forced re-login (e.g., `8h`, `24h`, `168h`). Omit, or set to `0s`, for unlimited session duration |
| `providers` | array | List of auth providers (currently only the first is used) |

#### Provider Settings

| Field | Type | Description |
| -------------------- | ------- | --------------------------------------------------------------- |
| `label` | string | Display name for the provider |
| `type` | string | Provider type. Only `oidc` is supported |
| `providerUrl` | string | OIDC discovery URL (e.g., `https://accounts.google.com/`) |
| `issuerUrl` | string | Optional. Set only if issuer differs from provider URL |
| `clientId` | string | OAuth2 client ID |
| `clientSecret` | string | OAuth2 client secret |
| `scopes` | array | OAuth2 scopes. Include `offline_access` to enable token refresh |
| `callbackUrl` | string | OAuth2 callback URL for your deployment |
| `options` | object | Additional URL parameters for the auth redirect |
| `useIdTokenAsBearer` | boolean | Use ID token instead of access token in Authorization header |
| Field | Type | Description |
| ---------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label` | string | Display name for the provider |
| `type` | string | Provider type. Only `oidc` is supported |
| `providerUrl` | string | OIDC discovery URL (e.g., `https://accounts.google.com/`) |
| `issuerUrl` | string | Optional. Set only if issuer differs from provider URL |
| `clientId` | string | OAuth2 client ID |
| `clientSecret` | string | OAuth2 client secret |
| `scopes` | array | OAuth2 scopes. Include `offline_access` to enable token refresh |
| `callbackUrl` | string | OAuth2 callback URL for your deployment |
| `options` | object | Additional URL parameters for the auth redirect |
| `useIdTokenAsBearer` | boolean | Use ID token instead of access token in Authorization header |
| `refreshTokenDuration` | duration | Lifetime of the refresh token this provider issues. Only needed for providers that issue opaque refresh tokens. See [Refresh token lifetime](#refresh-token-lifetime) |

#### Docker Environment Variables

The bundled `docker.yaml` maps each auth setting to an environment variable, so a
Docker deployment can be configured without supplying a custom config file.

| Environment Variable | Config Field | Default |
| -------------------------------------- | -------------------------------------------- | ----------------- |
| `TEMPORAL_AUTH_ENABLED` | `auth.enabled` | `false` |
| `TEMPORAL_AUTH_REDIRECT_TO_PROVIDER` | `auth.redirectToProvider` | `false` |
| `TEMPORAL_MAX_SESSION_DURATION` | `auth.maxSessionDuration` | unset (unlimited) |
| `TEMPORAL_AUTH_LABEL` | `auth.providers[0].label` | `sso` |
| `TEMPORAL_AUTH_TYPE` | `auth.providers[0].type` | `oidc` |
| `TEMPORAL_AUTH_PROVIDER_URL` | `auth.providers[0].providerUrl` | unset |
| `TEMPORAL_AUTH_ISSUER_URL` | `auth.providers[0].issuerUrl` | unset |
| `TEMPORAL_AUTH_CLIENT_ID` | `auth.providers[0].clientId` | unset |
| `TEMPORAL_AUTH_CLIENT_SECRET` | `auth.providers[0].clientSecret` | unset |
| `TEMPORAL_AUTH_CALLBACK_URL` | `auth.providers[0].callbackUrl` | unset |
| `TEMPORAL_AUTH_SCOPES` | `auth.providers[0].scopes` (comma separated) | unset |
| `TEMPORAL_AUTH_USE_ID_TOKEN_AS_BEARER` | `auth.providers[0].useIdTokenAsBearer` | `false` |
| `TEMPORAL_AUTH_REFRESH_TOKEN_DURATION` | `auth.providers[0].refreshTokenDuration` | unset |

## Session Duration Management

Expand Down Expand Up @@ -83,7 +105,10 @@ auth:
- `8h` - 8 hours (typical workday)
- `24h` - 24 hours
- `168h` - 1 week
- `0` or omitted - No maximum (session lasts until refresh token expires)
- `0s` or omitted - No maximum (session lasts until refresh token expires)

> Write an explicit zero as `0s`, not `0`. A bare `0` is parsed as a number rather
> than a duration and the server rejects the config file.

### How It Works

Expand All @@ -93,6 +118,41 @@ auth:

This is useful for compliance requirements where users must periodically re-verify their identity, independent of token validity.

The `user*` cookies that carry the access token to the browser are also held to the
session boundary: they are issued for one minute, or for whatever is left of the
session if that is shorter. Without this, a refresh performed shortly before the
boundary would hand the browser a full minute of cookie, leaving the UI looking
signed in while every API call behind it returned 401.

## Refresh Token Lifetime

The `refresh` cookie must outlive the access token, since its whole purpose is to
obtain the next one. Its lifetime is taken from the first of these that is available:

1. The `exp` claim of the refresh token itself, when the provider issues a JWT
refresh token. Keycloak and similar providers are handled automatically, with no
configuration.
2. The `refreshTokenDuration` configured for the provider. This is the setting for
providers that issue **opaque** refresh tokens, whose lifetime the server has no
way to read.
3. A default of 7 days.

Whichever applies, the cookie is capped at 30 days.

```yaml
auth:
providers:
- label: My IdP
# ...
refreshTokenDuration: 24h # match your IdP's refresh token lifetime
```

Note that this is not derived from the token response's `expires_in`. That field
describes the **access** token, per [RFC 6749 section 5.1](https://datatracker.ietf.org/doc/html/rfc6749#section-5.1)
and [OIDC Core section 3.2.2.5](https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.2.2.5),
and using it would expire the refresh cookie at the same moment as the token it
exists to replace.

## Provider-Specific Configuration

### Azure AD / Entra ID
Expand Down Expand Up @@ -220,6 +280,13 @@ Ensure:
- Refresh tokens are enabled in your IdP configuration
- The refresh token hasn't expired (check IdP settings)

If refresh starts failing with 401 at the moment the access token expires, check
the `refresh` cookie in browser devtools. A Max-Age matching the access token
lifetime means the cookie is being dropped before it can be used. Providers that
issue JWT refresh tokens are handled automatically; for one that issues opaque
refresh tokens, set `refreshTokenDuration` on the provider to its refresh token
lifetime. See [Refresh token lifetime](#refresh-token-lifetime).

### Redirect loop after login

Verify:
Expand Down
2 changes: 2 additions & 0 deletions server/config/docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ uiServerTLS:
auth:
enabled: {{ env "TEMPORAL_AUTH_ENABLED" | default "false" }}
redirectToProvider: {{ env "TEMPORAL_AUTH_REDIRECT_TO_PROVIDER" | default "false" }}
maxSessionDuration: {{ env "TEMPORAL_MAX_SESSION_DURATION" | default "" }}
providers:
- label: {{ env "TEMPORAL_AUTH_LABEL" | default "sso" }}
type: {{ env "TEMPORAL_AUTH_TYPE" | default "oidc" }}
Expand All @@ -61,6 +62,7 @@ auth:
clientSecret: {{ env "TEMPORAL_AUTH_CLIENT_SECRET" }}
callbackUrl: {{ env "TEMPORAL_AUTH_CALLBACK_URL" }}
useIdTokenAsBearer: {{ env "TEMPORAL_AUTH_USE_ID_TOKEN_AS_BEARER" | default "false" }}
refreshTokenDuration: {{ env "TEMPORAL_AUTH_REFRESH_TOKEN_DURATION" | default "" }}
scopes:
{{- if env "TEMPORAL_AUTH_SCOPES" }}
{{- range env "TEMPORAL_AUTH_SCOPES" | split "," }}
Expand Down
49 changes: 49 additions & 0 deletions server/config/e2e-auth.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# =============================================================================
# Auth configuration for the Playwright E2E suite.
#
# This is the counterpart to with-auth.yaml, which is tuned for `pnpm
# dev:with-auth` and a human clicking through the UI. The durations here are
# deliberately short so that tests/e2e/auth-cookie-lifetimes.spec.ts can wait
# out an access token in a few seconds, and so that maxSessionDuration is below
# the 60s user cookie lifetime and the clamp on those cookies is observable.
#
# The matching identity provider TTLs are set in tests/global-setup.ts via the
# OIDC_*_TTL environment variables.
# =============================================================================
publicPath:
port: 8081
enableUi: true
cors:
cookieInsecure: true
allowOrigins:
- http://localhost:8081
refreshInterval: 1m
defaultNamespace: default
showTemporalSystemNamespace: false
disableWriteActions: false
auth:
enabled: true
redirectToProvider: false
# Below the 60s user cookie lifetime, so that the clamp is visible at login.
maxSessionDuration: 45s
providers:
- label: E2E OIDC
type: oidc
providerUrl: http://localhost:8889
issuerUrl: ''
clientId: temporal-ui
clientSecret: temporal-secret
scopes:
- openid
- profile
- email
- offline_access
callbackUrl: http://localhost:8081/auth/sso/callback
# The mock provider issues opaque refresh tokens, so this is the value the
# refresh cookie should fall back to. A provider issuing JWT refresh
# tokens would have its own exp claim take precedence over this.
refreshTokenDuration: 24h
codec:
endpoint:
passAccessToken: false
includeCredentials: false
4 changes: 2 additions & 2 deletions server/config/with-auth.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ refreshWorkflowCountsDisabled: false
# This forces refresh before session expiry. Current: 2m session, 1m tokens.
# 2. Session Expiry: Wait for maxSessionDuration to elapse. User will be
# redirected to login and must re-authenticate at the OIDC provider.
# 3. No Session Limit: Set maxSessionDuration to 0 or remove it entirely.
# 3. No Session Limit: Set maxSessionDuration to 0s or remove it entirely.
# Tokens will refresh indefinitely until the OIDC refresh token expires.
# =============================================================================
auth:
enabled: true
redirectToProvider: false
maxSessionDuration: 2m # Max time before forced re-login (0 = unlimited)
maxSessionDuration: 2m # Max time before forced re-login (0s = unlimited)
providers:
- label: Dummy OIDC
type: oidc # for futureproofing; only oidc is supported today
Expand Down
32 changes: 32 additions & 0 deletions server/plugins/fs_config_provider/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"io/ioutil"
"os"
"testing"
"time"

"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
Expand Down Expand Up @@ -165,3 +166,34 @@ func buildConfig(env string) string {
item1: ` + item1 + `
item2: ` + item2
}

// TestDockerConfigSessionDefaultsAreUnset pins the defaults for the auth duration
// settings in docker.yaml. Both must render to an unset duration, so that adding
// them does not silently start expiring sessions, or shorten refresh cookies, for
// Docker deployments that set neither environment variable.
func TestDockerConfigSessionDefaultsAreUnset(t *testing.T) {
t.Setenv("TEMPORAL_AUTH_ENABLED", "true")

cfg, err := LoadConfig("../../config", "docker")
require.NoError(t, err)

require.Zero(t, cfg.Auth.MaxSessionDuration, "TEMPORAL_MAX_SESSION_DURATION must default to unset")
require.Len(t, cfg.Auth.Providers, 1)
require.Zero(t, cfg.Auth.Providers[0].RefreshTokenDuration, "TEMPORAL_AUTH_REFRESH_TOKEN_DURATION must default to unset")
}

// TestDockerConfigSessionDurationsFromEnv covers the reason these fields were added
// to docker.yaml: without them a Docker operator has no way to set either value
// short of supplying a wholly custom config file.
func TestDockerConfigSessionDurationsFromEnv(t *testing.T) {
t.Setenv("TEMPORAL_AUTH_ENABLED", "true")
t.Setenv("TEMPORAL_MAX_SESSION_DURATION", "8h")
t.Setenv("TEMPORAL_AUTH_REFRESH_TOKEN_DURATION", "24h")

cfg, err := LoadConfig("../../config", "docker")
require.NoError(t, err)

require.Equal(t, 8*time.Hour, cfg.Auth.MaxSessionDuration)
require.Len(t, cfg.Auth.Providers, 1)
require.Equal(t, 24*time.Hour, cfg.Auth.Providers[0].RefreshTokenDuration)
}
Loading
Loading