Skip to content

Commit 50dcd81

Browse files
committed
docs(auth): add JWT proxy auth page
Document JWT proxy authentication for reverse proxy setups (Pomerium, Cloudflare Access, etc). Covers auth flow, config file/env vars, Docker example, options table, supported algorithms, and JWKS retry. Ref: semaphoreui/semaphore#3719
1 parent e46bf1b commit 50dcd81

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

docs/admin-guide/jwt-proxy-auth.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# JWT Proxy Authentication
2+
3+
When Semaphore runs behind an authenticating reverse proxy (e.g. Pomerium, Cloudflare Access, or OAuth2 Proxy), the proxy can pass user identity via a signed JWT header. Semaphore validates the JWT against a JWKS endpoint and creates or looks up the user automatically.
4+
5+
This avoids duplicating OIDC configuration between the proxy and Semaphore.
6+
7+
## How it works
8+
9+
1. The reverse proxy authenticates the user and adds a signed JWT to a configured HTTP header.
10+
2. Semaphore checks for this header on every request. If present, the token is validated against the JWKS endpoint.
11+
3. If the token is valid, the user is loaded by email. If no matching user exists, an external user is created automatically (same as OIDC).
12+
4. If the header is present but the token is invalid, Semaphore returns `401 Unauthorized` immediately -- existing bearer token and session auth are not attempted.
13+
5. If the header is absent, normal authentication (bearer token, session cookie) proceeds as usual.
14+
15+
Users created via JWT auth are marked as **external**. If a JWT email matches a local (non-external) user, authentication is rejected.
16+
17+
## Configuration
18+
19+
Both `header` and `jwks_url` are required when JWT auth is enabled. Semaphore validates this at startup.
20+
21+
### Config file
22+
23+
```json
24+
{
25+
"auth": {
26+
"jwt": {
27+
"enabled": true,
28+
"header": "X-Pomerium-Jwt-Assertion",
29+
"jwks_url": "https://auth.example.com/.well-known/pomerium/jwks.json",
30+
"audience": "https://semaphore.example.com",
31+
"issuer": "https://auth.example.com"
32+
}
33+
}
34+
}
35+
```
36+
37+
### Environment variables
38+
39+
```bash
40+
SEMAPHORE_JWT_AUTH_ENABLED=true
41+
SEMAPHORE_JWT_AUTH_HEADER=X-Pomerium-Jwt-Assertion
42+
SEMAPHORE_JWT_AUTH_JWKS_URL=https://auth.example.com/.well-known/pomerium/jwks.json
43+
SEMAPHORE_JWT_AUTH_AUDIENCE=https://semaphore.example.com
44+
SEMAPHORE_JWT_AUTH_ISSUER=https://auth.example.com
45+
```
46+
47+
### Docker example
48+
49+
```bash
50+
docker run -d -p 3000:3000 --name semaphore \
51+
-e SEMAPHORE_DB_DIALECT=bolt \
52+
-e SEMAPHORE_ADMIN=admin \
53+
-e SEMAPHORE_ADMIN_PASSWORD=changeme \
54+
-e SEMAPHORE_ADMIN_NAME=Admin \
55+
-e SEMAPHORE_ADMIN_EMAIL=admin@localhost \
56+
-e SEMAPHORE_JWT_AUTH_ENABLED=true \
57+
-e SEMAPHORE_JWT_AUTH_HEADER=X-Pomerium-Jwt-Assertion \
58+
-e SEMAPHORE_JWT_AUTH_JWKS_URL=https://auth.example.com/.well-known/pomerium/jwks.json \
59+
-e SEMAPHORE_JWT_AUTH_AUDIENCE=https://semaphore.example.com \
60+
-e SEMAPHORE_JWT_AUTH_ISSUER=https://auth.example.com \
61+
semaphoreui/semaphore:latest
62+
```
63+
64+
## Options
65+
66+
| Parameter | Environment Variable | Description |
67+
| --- | --- | --- |
68+
| `auth.jwt.enabled` | `SEMAPHORE_JWT_AUTH_ENABLED` | Enable JWT proxy authentication. |
69+
| `auth.jwt.header` | `SEMAPHORE_JWT_AUTH_HEADER` | HTTP header containing the JWT. **Required.** |
70+
| `auth.jwt.jwks_url` | `SEMAPHORE_JWT_AUTH_JWKS_URL` | URL of the JWKS endpoint for signature verification. **Required.** |
71+
| `auth.jwt.audience` | `SEMAPHORE_JWT_AUTH_AUDIENCE` | Expected `aud` claim. If empty, audience is not validated. |
72+
| `auth.jwt.issuer` | `SEMAPHORE_JWT_AUTH_ISSUER` | Expected `iss` claim. If empty, issuer is not validated. |
73+
| `auth.jwt.email_claim` | `SEMAPHORE_JWT_AUTH_EMAIL_CLAIM` | JWT claim for the user's email. Default: `email`. |
74+
| `auth.jwt.name_claim` | `SEMAPHORE_JWT_AUTH_NAME_CLAIM` | JWT claim for the user's display name. Default: `name`. |
75+
| `auth.jwt.username_claim` | `SEMAPHORE_JWT_AUTH_USERNAME_CLAIM` | JWT claim for the username. Default: `email`. |
76+
77+
## Supported algorithms
78+
79+
Semaphore accepts tokens signed with **ES256**, **ES384**, **ES512**, **RS256**, **RS384**, or **RS512**.
80+
81+
## JWKS availability
82+
83+
Semaphore fetches the JWKS on startup. If the JWKS endpoint is unreachable, the server still starts and retries in the background (5s, 10s, 30s, then every 60s). JWT auth returns an error until the JWKS is loaded.

sidebars.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ const sidebars = {
7979
],
8080
},
8181
'admin-guide/ldap',
82+
'admin-guide/jwt-proxy-auth',
8283
{
8384
type: 'category',
8485
label: 'OpenID Connect',

0 commit comments

Comments
 (0)