Skip to content
Open
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
39 changes: 39 additions & 0 deletions AUTHENTICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ auth:
| -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `enabled` | boolean | Enable or disable authentication |
| `maxSessionDuration` | duration | Maximum session duration before forced re-login (e.g., `8h`, `24h`, `168h`). Set to `0` or omit for unlimited session duration |
| `pkceEnabled` | boolean | Enable PKCE (Proof Key for Code Exchange) for the OAuth2 authorization code flow. Default: `false` |
| `providers` | array | List of auth providers (currently only the first is used) |

#### Provider Settings
Expand All @@ -56,6 +57,44 @@ auth:
| `options` | object | Additional URL parameters for the auth redirect |
| `useIdTokenAsBearer` | boolean | Use ID token instead of access token in Authorization header |

## PKCE Support

Temporal UI supports PKCE (Proof Key for Code Exchange, RFC 7636) for the OAuth2 authorization code flow. PKCE provides protection against authorization code interception attacks and is recommended for all OAuth2 applications, especially those that cannot securely store a client secret (e.g., public clients).

### Configuration

```yaml
auth:
enabled: true
pkceEnabled: true # Enable PKCE
providers:
# ... provider config
```

PKCE can also be enabled via environment variable:

| Environment Variable | Default | Description |
| ---------------------------- | ------- | ---------------------------- |
| `TEMPORAL_AUTH_PKCE_ENABLED` | `false` | Set to `true` to enable PKCE |

### How It Works

When PKCE is enabled:

1. The UI server generates a cryptographically random `code_verifier` and stores it in a secure, HttpOnly cookie
2. A `code_challenge` (SHA-256 hash of the verifier) is included in the authorization request as `code_challenge_method=S256`
3. Upon receiving the authorization code in the callback, the server sends the original `code_verifier` to the token endpoint
4. The identity provider verifies that the challenge matches the verifier, ensuring the party exchanging the code is the same one that initiated the request

### When to Enable

Enabling PKCE is recommended:

- When using public clients or single-page applications
- To mitigate authorization code interception attacks
- As a defense-in-depth measure even for confidential clients with a client secret
- When required by your identity provider (many modern IdPs strongly recommend or require PKCE)

## Session Duration Management

### Token Refresh vs Session Duration
Expand Down
1 change: 1 addition & 0 deletions server/config/docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ uiServerTLS:
keyFile: {{ env "TEMPORAL_UI_SERVER_TLS_KEY" | default "" }}
auth:
enabled: {{ env "TEMPORAL_AUTH_ENABLED" | default "false" }}
pkceEnabled: {{ env "TEMPORAL_AUTH_PKCE_ENABLED" | default "false" }}
providers:
- label: {{ env "TEMPORAL_AUTH_LABEL" | default "sso" }}
type: {{ env "TEMPORAL_AUTH_TYPE" | default "oidc" }}
Expand Down
20 changes: 16 additions & 4 deletions server/server/auth/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type Claims struct {
Picture string `json:"picture"`
}

func ExchangeCode(ctx context.Context, r *http.Request, config *oauth2.Config, provider *oidc.Provider) (*User, error) {
func ExchangeCode(ctx context.Context, r *http.Request, config *oauth2.Config, provider *oidc.Provider, pkceEnabled bool) (*User, error) {
state, err := r.Cookie("state")
if err != nil {
return nil, echo.NewHTTPError(http.StatusBadRequest, "State cookie is not set in request")
Expand All @@ -67,9 +67,21 @@ func ExchangeCode(ctx context.Context, r *http.Request, config *oauth2.Config, p
return nil, echo.NewHTTPError(http.StatusBadRequest, "State cookie did not match")
}

oauth2Token, err := config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
return nil, echo.NewHTTPError(http.StatusInternalServerError, "Unable to exchange token: "+err.Error())
var oauth2Token *oauth2.Token
if pkceEnabled {
codeVerifier, err := r.Cookie("code_verifier")
if err != nil {
return nil, echo.NewHTTPError(http.StatusBadRequest, "Code verifier is not set in request")
}
oauth2Token, err = config.Exchange(ctx, r.URL.Query().Get("code"), oauth2.SetAuthURLParam("code_verifier", codeVerifier.Value))
if err != nil {
return nil, echo.NewHTTPError(http.StatusInternalServerError, "Unable to exchange token: "+err.Error())
}
} else {
oauth2Token, err = config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
return nil, echo.NewHTTPError(http.StatusInternalServerError, "Unable to exchange token: "+err.Error())
}
}

rawIDToken, ok := oauth2Token.Extra("id_token").(string)
Expand Down
Loading
Loading