CrowdSieve supports two authentication modes for the dashboard:
| Mode | When to use |
|---|---|
oidc |
Standard OpenID Connect deployment. The dashboard handles the login flow itself via the iron-session cookie. Documented in this page. |
headers |
A trusted reverse proxy (LemonLDAP-NG handler, NGINX auth_request, Apache mod_auth_*, etc.) authenticates the user upstream and forwards their identity via Auth-* headers. |
none |
No authentication enforced. Default when neither OIDC nor headers mode is configured. |
The mode is selected via AUTH_MODE (oidc | headers | none). When unset, the dashboard auto-detects: oidc if OIDC_ISSUER and OIDC_CLIENT_ID are set, otherwise none. Jump to HTTP Headers Mode for the proxy-based setup.
CrowdSieve supports OpenID Connect (OIDC) authentication for the dashboard. When configured, users must authenticate via an OIDC provider to access the dashboard.
sequenceDiagram
participant User
participant Dashboard as CrowdSieve Dashboard
participant Provider as OIDC Provider
User->>Dashboard: 1. Access dashboard
Dashboard->>User: 2. Redirect to provider
User->>Provider: 3. Follow redirect
User->>Provider: 4. Authenticate (login)
Provider->>User: 5. Redirect with auth code
User->>Dashboard: 6. Callback with auth code
alt client_secret (default)
Dashboard->>Provider: 7a. Exchange code + client_secret
else private_key_jwt (JWS enabled)
Dashboard->>Provider: 7b. Exchange code + signed JWT
end
alt Plain token (default)
Provider->>Dashboard: 8a. ID Token (JWT)
else Encrypted token (JWE enabled)
Provider->>Dashboard: 8b. ID Token (JWE)
Dashboard->>Dashboard: Decrypt with private key
end
Dashboard->>Dashboard: 9. Create session
Dashboard->>User: 10. Authenticated
| Variable | Required | Default | Description |
|---|---|---|---|
AUTH_MODE |
No | auto | oidc, headers, or none. Auto-detects to oidc when OIDC_ISSUER + OIDC_CLIENT_ID are set, otherwise none. |
OIDC_ISSUER |
Yes* | - | OIDC provider URL (e.g., https://auth.example.com/). *Required for OIDC mode. |
OIDC_CLIENT_ID |
Yes* | - | OAuth2 client ID. *Required for OIDC mode. |
OIDC_CLIENT_SECRET |
No* | - | OAuth2 client secret (*required for OIDC unless JWS is enabled). |
AUTH_ACTOR_CLAIM |
No | sub |
Claim used as audit-log actor. Any non-empty string (e.g. sub, email, name, preferredUsername, familyName). Wins over OIDC_ACTOR_CLAIM when both are set. |
OIDC_ACTOR_CLAIM |
No | sub |
Legacy alias for AUTH_ACTOR_CLAIM. Kept for back-compat. |
SESSION_SECRET |
Yes* | - | Session encryption key (minimum 32 characters). *Required for OIDC mode. |
SESSION_COOKIE_SECURE |
No | true |
Set to false for HTTP-only development. |
NEXTAUTH_URL |
No | auto | Base URL for callbacks (auto-detected if not set). |
TRUSTED_PROXY_IPS |
No | - | (Headers mode only) Comma-separated allowlist of upstream-proxy IPs / IPv4 CIDR ranges. Empty = no IP check. |
AUTH_LOGOUT_URL |
No | - | (Headers mode only) External logout URL the dashboard redirects to when a user clicks "Sign out". When unset, the link is hidden. |
AUTH_LOGIN_URL |
No | - | (Headers mode only) External login URL surfaced as a "Go to login portal" button on the headers-mode 401 page. |
The AUTH_ACTOR_CLAIM (or legacy OIDC_ACTOR_CLAIM) variable controls which
claim identifies the user that issued or revoked a decision (recorded on every
manual ban and unban event, and on the matching local audit rows persisted in
the alerts table). Defaults to sub (always present, stable, opaque). Set to
email, name, preferredUsername, etc. for a more human-readable audit
trail. If the configured claim is missing on the user, the code falls back to
sub. Any non-empty string is accepted, which makes this work seamlessly with
claims forwarded by HTTP Headers Mode
such as familyName or preferredUsername.
Generate secrets with:
# Session secret
openssl rand -hex 32
# Client secret (if needed)
openssl rand -hex 32# Required
OIDC_ISSUER=https://auth.example.com/
OIDC_CLIENT_ID=crowdsieve-dashboard
OIDC_CLIENT_SECRET=your-client-secret
SESSION_SECRET=your-32-char-minimum-session-secret
# Optional
SESSION_COOKIE_SECURE=true # Set to false for HTTP
NEXTAUTH_URL=https://crowdsieve.example.comThe default authentication method uses a shared secret between CrowdSieve and the OIDC provider.
OIDC_CLIENT_SECRET=your-client-secretThe provider must be configured with:
- Client authentication:
client_secret_postorclient_secret_basic
For stronger security, use private_key_jwt authentication (RFC 7523). CrowdSieve signs a JWT assertion with its private key instead of using a shared secret.
JWS_ENABLED=true
JWS_KEY_ALG=RS256 # Default, also supports ES256, ES384, ES512
JWE_KEYS_PATH=./data/jwks.json # Persist keys across restartsBenefits:
- No shared secret to manage
- Asymmetric cryptography (more secure)
- Required by some high-security providers
Provider configuration:
- Client authentication:
private_key_jwt - Import public keys from:
https://crowdsieve.example.com/api/jwks
Note: When
JWS_ENABLED=true,OIDC_CLIENT_SECRETis ignored. A warning is logged if both are configured.
Enable JWE to decrypt encrypted tokens from the OIDC provider. This provides an additional layer of security by encrypting the ID token and logout token contents.
JWE_ENABLED=true
JWE_KEY_ALG=RSA-OAEP-256 # Default
JWE_CONTENT_ALGS=A256GCM,A128GCM # Content encryption algorithms
JWE_KEYS_PATH=./data/jwks.json # Persist keys across restarts
JWE_KEY_ROTATION_DAYS=30 # Optional: auto-rotate keysSupported tokens:
- ID tokens (from authorization code exchange)
- Back-channel logout tokens
Provider configuration:
- Enable "Encrypt ID token"
- Import public encryption key from:
https://crowdsieve.example.com/api/jwks - Key encryption algorithm:
RSA-OAEP-256 - Content encryption algorithm:
A256GCM
When JWS or JWE is enabled, CrowdSieve manages cryptographic keys automatically.
JWE_KEYS_PATH=./data/jwks.jsonImportant: Always configure JWE_KEYS_PATH in production. Without it, keys are regenerated on each restart, invalidating encrypted sessions.
JWE_KEY_ROTATION_DAYS=30 # Rotate every 30 daysWhen rotation is enabled:
- Signing keys (JWS): 3 keys are published (next, current, previous) for seamless rotation
- Encryption keys (JWE): Current key is published, previous key is kept for decryption
Public keys are published at:
GET /api/jwks
Response format:
{
"keys": [
{
"kty": "RSA",
"kid": "crowdsieve-sig-...",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "crowdsieve-enc-...",
"use": "enc",
"alg": "RSA-OAEP-256",
"n": "...",
"e": "AQAB"
}
]
}CrowdSieve supports OpenID Connect Back-Channel Logout 1.0 for single sign-out. When a user logs out from the OIDC provider, the matching dashboard session is terminated.
Hard prerequisite (IdP side): the OIDC provider MUST issue id_tokens that carry a
sidclaim. The dashboard storesclaims.sidon the session cookie at callback time and revocation is keyed on it: a session without asidcannot be revoked at all. The logout token MUST also carry a matchingsid(the in-memory revocation store is indexed bysid).
Note: this endpoint lives on the Next.js dashboard, not on the Fastify backend, so it does not appear in the generated
openapi.json(which only covers the proxy/management API). The reference below is the canonical contract.
| Item | Value |
|---|---|
| Method | POST |
| Path | /api/auth/backchannel-logout |
| Public URL | https://crowdsieve.example.com/api/auth/backchannel-logout |
| Content-Type | application/x-www-form-urlencoded |
| Authentication | None at the HTTP layer — the request is authenticated by the JWT signature on logout_token, verified against the provider's JWKS (fetched from <issuer>/.well-known/openid-configuration). |
| Caller | Direct call from the OIDC provider (server-to-server, no browser involvement). |
A single form-encoded parameter:
| Field | Type | Required | Description |
|---|---|---|---|
logout_token |
string | Yes | A JWS or JWE token issued by the OIDC provider. Compact serialization (3 parts for JWS, 5 parts for JWE). |
The token is decrypted (when JWE is enabled and the token has 5 segments) then signature-verified. Claims are validated per the spec:
| Claim | Required | Validation |
|---|---|---|
iss |
Yes | Must match the configured OIDC_ISSUER. |
aud |
Yes | Must match the configured OIDC_CLIENT_ID (single audience or array containing it). |
iat |
Yes | Issued-at timestamp. |
jti |
Yes | Unique token id; tracked in memory for 5 minutes to reject replays. Tokens missing jti are rejected. |
events |
Yes | Must contain the key http://schemas.openid.net/event/backchannel-logout with an empty object value (per spec). |
sid |
Yes (in practice) | Session identifier. Required for revocation to take effect (see prerequisite above) — must match claims.sid of the original id_token. |
sub |
At least one of sub/sid per spec |
Subject identifier. The endpoint accepts sub-only logout tokens, but they only revoke if the session itself carries a sid. |
nonce |
Forbidden | Per spec a logout token MUST NOT carry a nonce; the request is rejected if it does. |
| Status | When | Body |
|---|---|---|
200 OK |
Token verified and (if applicable) matching session(s) revoked. Empty body, Cache-Control: no-store per spec. A 200 is also returned when no session matched — revocation is best-effort. |
(empty) |
400 Bad Request |
Missing/empty logout_token, JWE decryption failure, signature failure, claim validation failure (issuer / audience / event / nonce-forbidden / sub-or-sid missing), jti reuse (replay). |
Plain-text error message (e.g. Invalid logout_token: …). |
404 Not Found |
OIDC mode is not enabled on this dashboard. | Not Found |
500 Internal Server Error |
OIDC is flagged as enabled but the runtime config is missing. | OIDC not configured |
- Per-session revocation, keyed on the
sidstored in the session cookie at login time. - Support for encrypted logout tokens (JWE), decrypted with the dashboard's private key from
JWE_KEYS_PATH. - Signature verification via the provider's JWKS (key rotation handled transparently by
jose). - Replay protection: each accepted
jtiis rejected for the next 5 minutes.
- Back-channel logout URL:
https://crowdsieve.example.com/api/auth/backchannel-logout - Back-channel logout session required:
ON— this is what makes the IdP includesidin id_tokens and in logout tokens. Without it, revocation will be a no-op.
# Build a logout token with your provider's signing key, then:
curl -i -X POST https://crowdsieve.example.com/api/auth/backchannel-logout \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "logout_token=$LOGOUT_TOKEN"
# Expected: HTTP/1.1 200 OK with `Cache-Control: no-store`.| Type | Default | Supported |
|---|---|---|
| JWS Signing | RS256 |
RS256, RS384, RS512, ES256, ES384, ES512, EdDSA |
| JWE Key Encryption | RSA-OAEP-256 |
RSA-OAEP, RSA-OAEP-256, RSA-OAEP-384, RSA-OAEP-512 |
| JWE Content Encryption | A256GCM |
A256GCM, A128GCM, A192GCM |
Configure these URLs in your OIDC provider:
| Endpoint | URL | Description |
|---|---|---|
| Callback | https://crowdsieve.example.com/api/auth/callback/oidc |
OAuth2 redirect after login |
| JWKS | https://crowdsieve.example.com/api/jwks |
Public keys for JWE/JWS |
| Back-channel Logout | https://crowdsieve.example.com/api/auth/backchannel-logout |
SSO logout notification |
| Post-logout Redirect | https://crowdsieve.example.com |
Redirect after logout |
-
Create a new OpenID Connect Relying Party in the Manager:
- Go to
OpenID Connect Relying Parties>Add a new Relying Party - Client ID:
crowdsieve-dashboard
- Go to
-
Configure the Relying Party in the
Optionstab:Setting Value Allowed redirection addresses https://crowdsieve.example.com/api/auth/callback/oidcAllowed post-logout redirection addresses https://crowdsieve.example.com -
Configure authentication in the
Securitytab:- With client_secret: Set authentication method to
client_secret_postorclient_secret_basic, and set a client secret - With private_key_jwt (JWS): Set authentication method to
private_key_jwt, and import public key fromhttps://crowdsieve.example.com/api/jwks
- With client_secret: Set authentication method to
-
Enable Back-channel Logout (optional):
- In
Logouttab, set Back-channel logout URL:https://crowdsieve.example.com/api/auth/backchannel-logout
- In
-
Enable ID Token Encryption (optional, requires JWE):
- In
Securitytab, enable "Encrypt ID token" - Set ID token encryption algorithm:
RSA-OAEP-256 - Set ID token encryption content algorithm:
A256GCM - Import public key from:
https://crowdsieve.example.com/api/jwks
- In
-
Issuer URL:
https://auth.example.com(your LemonLDAP::NG portal URL)
-
Create a new client in your realm:
- Client ID:
crowdsieve-dashboard - Client Protocol:
openid-connect - Access Type:
confidential
- Client ID:
-
Configure URLs in the client settings:
Setting Value Root URL https://crowdsieve.example.comValid Redirect URIs https://crowdsieve.example.com/api/auth/callback/oidcValid Post Logout Redirect URIs https://crowdsieve.example.comWeb Origins https://crowdsieve.example.com -
Configure authentication in the Credentials tab:
- With client_secret: Use "Client Id and Secret" and copy the secret
- With private_key_jwt (JWS): Use "Signed JWT", import JWKS from
https://crowdsieve.example.com/api/jwks
-
Enable Back-channel Logout (optional):
- Back-channel logout URL:
https://crowdsieve.example.com/api/auth/backchannel-logout - Back-channel logout session required:
ON
- Back-channel logout URL:
-
Enable ID Token Encryption (optional, requires JWE):
- Go to client > Keys tab
- Enable "Encrypt ID token"
- Import keys from JWKS URL:
https://crowdsieve.example.com/api/jwks
-
Issuer URL:
https://keycloak.example.com/realms/{realm}
| Provider | Issuer URL Format |
|---|---|
| LemonLDAP::NG | https://auth.example.com |
| Keycloak | https://keycloak.example.com/realms/{realm} |
| Auth0 | https://{tenant}.auth0.com |
| Okta | https://{domain}.okta.com |
https://accounts.google.com |
|
| Azure AD | https://login.microsoftonline.com/{tenant}/v2.0 |
For Kubernetes deployments, see the Helm chart documentation.
Basic Helm values:
crowdsieve:
dashboard:
oidc:
enabled: true
issuer: 'https://auth.example.com'
clientId: 'crowdsieve-dashboard'
clientSecret: 'your-client-secret'
session:
secret: '32-chars-minimum-secret-here!!'
cookieSecure: trueWith private_key_jwt (JWS):
crowdsieve:
dashboard:
oidc:
enabled: true
issuer: 'https://auth.example.com'
clientId: 'crowdsieve-dashboard'
# No clientSecret - using private_key_jwt
session:
secret: '32-chars-minimum-secret-here!!'
keys:
jwsEnabled: true
jwsAlgorithm: 'RS256'
rotationDays: 30"OIDC not configured"
- Ensure
OIDC_ISSUER,OIDC_CLIENT_ID, andSESSION_SECRETare set - Check that the issuer URL is reachable
"Invalid redirect URI"
- Verify the callback URL is registered in your provider:
https://crowdsieve.example.com/api/auth/callback/oidc
"Failed to decrypt token"
- Ensure
JWE_ENABLED=trueandJWE_KEYS_PATHis configured - Verify the provider has imported the correct public key from
/api/jwks - Check that the encryption algorithms match
"Invalid client authentication"
- If using
private_key_jwt, ensureJWS_ENABLED=true - Verify the provider has imported the signing key from
/api/jwks - Check that the provider is configured for
private_key_jwtauthentication
Sessions invalidated after restart
- Configure
JWE_KEYS_PATHto persist keys - In Helm, keys are stored at
/app/data/jwks.jsonon the PVC
Enable debug logging to troubleshoot authentication issues:
LOG_LEVEL=debugLook for:
OIDC: Using private_key_jwt authentication- JWS is activeJWE decryption enabled for OIDC responses- JWE is activeDecrypted encrypted logout token- JWE logout token processedBack-channel logout: revoked session- Logout notification received
Some deployments sit behind a "handler" reverse proxy (LemonLDAP-NG, NGINX auth_request, Apache mod_auth_*, Traefik ForwardAuth, etc.) that performs authentication upstream and forwards the user's identity to the protected app via HTTP headers. CrowdSieve supports this model via AUTH_MODE=headers.
Critical security requirement: the dashboard MUST be unreachable except via the trusted upstream proxy. Headers mode trusts every
Auth-*header it receives — if a client can reach the dashboard directly, they can spoof any identity. Use network isolation (Kubernetes NetworkPolicy, firewall rules, listening on127.0.0.1, etc.) and optionallyTRUSTED_PROXY_IPSfor defense in depth.
AUTH_MODE=headers
# Optional: only accept requests from these source IPs / IPv4 CIDR ranges.
# When unset, no IP check is performed (rely on network isolation).
TRUSTED_PROXY_IPS=10.0.0.5,10.42.0.0/16
# Optional: where to send the user when they click "Sign out".
# When unset, the Sign-out link is hidden.
AUTH_LOGOUT_URL=https://portal.example.com/logout
# Optional: shown on the dashboard's 401 page as a "Go to login portal" button.
AUTH_LOGIN_URL=https://portal.example.com/
# Optional: which claim to use as the audit-log actor (defaults to "sub").
AUTH_ACTOR_CLAIM=preferredUsernameIdentity is forwarded as Auth-<Field> headers. The dashboard maps each Auth-Foo-Bar-Baz header to a fooBarBaz claim on the SessionUser:
| Header | Claim | Notes |
|---|---|---|
Auth-Sub |
sub |
Required. Stable, opaque user id. |
Auth-Email |
email |
Optional. |
Auth-Name |
name |
Optional, full display name. |
Auth-Family-Name |
familyName |
Optional. |
Auth-Given-Name |
givenName |
Optional. |
Auth-Picture |
picture |
Optional. Only http:// / https:// accepted. |
Auth-Preferred-Username |
preferredUsername |
Optional, useful as AUTH_ACTOR_CLAIM. |
Auth-<Anything> |
<camelCase> |
Any custom field. Auth-Foo-Bar-Baz → fooBarBaz. |
Mapping rule: strip the auth- prefix (case-insensitive), then convert the remaining kebab-case suffix to camelCase. Header values are trimmed, stripped of CR/LF, and capped at 1024 characters as a defensive measure.
- A request without a non-empty
Auth-Subheader is rejected with HTTP 401. - When
TRUSTED_PROXY_IPSis set, a request from any other source IP is rejected with HTTP 403 before headers are even inspected. - The login page (
/login) renders a static "Authentication required" UI. The dashboard does not initiate a login flow — that is the upstream proxy's responsibility. - The "Sign out" link in the user menu is hidden unless
AUTH_LOGOUT_URLis configured. When it is, clicking the link redirects the user to that URL (typically the proxy's logout endpoint). - Sessions are stateless: there is no iron-session cookie, no server-side session store. Every request re-validates against the headers it carries.
- The audit-log actor (
X-Crowdsieve-Actorforwarded to the backend on bans/unbans) follows the configuredAUTH_ACTOR_CLAIM. With headers mode you can use any claim you forward, e.g.preferredUsernameoremail.
In the LemonLDAP-NG manager, configure a "Virtual Host" pointing at the dashboard with a handler-style protection rule:
# Headers (Manager > Virtual Hosts > <crowdsieve.example.com> > Headers)
Auth-Sub = $uid
Auth-Email = $mail
Auth-Name = $cn
Auth-Family-Name = $sn
Auth-Given-Name = $givenName
Auth-Preferred-Username = $uid
Then point your reverse proxy (NGINX, Apache, etc.) at the LemonLDAP-NG handler and forward to the CrowdSieve dashboard. See the LemonLDAP-NG documentation for the canonical setup.
server {
listen 443 ssl;
server_name crowdsieve.example.com;
# Forward auth to the upstream identity verifier.
location = /_auth {
internal;
proxy_pass https://auth.example.com/check;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
location / {
auth_request /_auth;
# Capture identity headers from the auth response.
auth_request_set $auth_sub $upstream_http_auth_sub;
auth_request_set $auth_email $upstream_http_auth_email;
auth_request_set $auth_name $upstream_http_auth_name;
# Forward them to the dashboard.
proxy_set_header Auth-Sub $auth_sub;
proxy_set_header Auth-Email $auth_email;
proxy_set_header Auth-Name $auth_name;
proxy_pass http://crowdsieve-dashboard:3000;
}
}TRUSTED_PROXY_IPS is a comma-separated allowlist applied to the immediate caller of the dashboard:
| Entry format | Match semantics |
|---|---|
10.0.0.5 |
Exact IPv4. |
10.0.0.0/16 |
IPv4 CIDR. |
2001:db8::1 |
Exact IPv6 (IPv6 CIDRs are matched as exact strings only in v1). |
::ffff:10.0.0.1 |
IPv4-mapped IPv6, normalized to the IPv4 form for comparison against IPv4/IPv4-CIDR entries. |
The client IP is resolved in this order:
- The Next.js
NextRequest.ipfield, if present. - The first non-empty entry of the
X-Forwarded-Forheader. - The
X-Real-IPheader.
When TRUSTED_PROXY_IPS is unset, no IP check is performed — operators are expected to ensure the dashboard is unreachable except via the proxy.