Authentication & Authorization¶
CloudEvent Player supports OAuth 2.0 authentication with Keycloak and role-based access control (RBAC) to secure your event generation and monitoring capabilities.
🚀 Quick Start: For a complete step-by-step guide to set up RBAC with Keycloak, see the RBAC Configuration Guide.
🔍 Mode Detection: The frontend automatically detects authentication mode from the authentication state, not from backend configuration. This makes the system resilient to configuration issues. See Technical Details below.
Overview¶
The authentication system provides:
- OAuth 2.0 / OIDC: Industry-standard authentication with Keycloak
- OAuth2 Proxy / Istio Support: Works with proxy-based authentication
- PKCE Flow: Secure authorization code flow for browser-based apps
- JWT Validation: Token-based authentication with RS256 signature verification
- Role-Based Access Control: Fine-grained permissions based on user roles
- Automatic Mode Detection: Frontend adapts to deployment environment
Authentication Modes¶
CloudEvent Player automatically detects and adapts to different authentication scenarios:
1. No Authentication (Default)¶
When no authentication is configured, the application runs in open mode with no access restrictions.
2. Istio/Proxy Mode (Production - Recommended)¶
Pre-authenticated mode where JWT tokens are injected by OAuth2 Proxy or Istio service mesh. The backend validates tokens but the frontend doesn't handle the login flow.
How it works:
- OAuth2 Proxy handles the OAuth flow with Keycloak
- OAuth2 Proxy stores tokens in encrypted cookie
- OAuth2 Proxy injects JWT in request headers to backend
- Backend validates JWT and extracts user information
- Frontend calls
/api/auth/infoand seesauthenticated: true - Frontend automatically enters "Istio mode" - no login button needed
docker run -p 8080:8080 \
-e AUTH_REQUIRED=true \
-e AUTH_TRUST_MODE=true \
ghcr.io/bvandewe/events-player:latest
Environment Variables:
AUTH_REQUIRED=true- Enable authenticationAUTH_TRUST_MODE=true- Skip JWT verification (proxy already validated)AUTH_JWKS_URL- Optional, for JWT validation if not using trust mode- No
OAUTH_SERVER_URLneeded - OAuth handled by proxy
Key Benefit: JWT never exposed to browser JavaScript (enhanced security)
3. OAuth/Keycloak Mode (Development)¶
Full OAuth 2.0 authentication with Keycloak for local development and standalone deployments.
How it works:
- Frontend calls
/api/auth/infoand receives OAuth configuration - Frontend shows login button when user not authenticated
- User clicks login → Redirects to Keycloak
- After successful login → Token stored in browser sessionStorage
- Frontend includes token in all API requests
docker run -p 8080:8080 \
-e AUTH_REQUIRED=true \
-e OAUTH_SERVER_URL=http://keycloak:8080 \
-e OAUTH_REALM=events-player \
-e OAUTH_CLIENT_ID=events-player-web \
ghcr.io/bvandewe/events-player:latest
User Roles¶
CloudEvent Player defines three user roles with different permission levels:
Admin Role¶
Full administrative access to all features.
Permissions:
- ✅ View event headers
- ✅ Expand and view event details
- ✅ Access generator panel
- ✅ Generate events with custom parameters
- ✅ Use iterations control (1-100 events)
- ✅ Use delay control (1-2000ms)
Operator Role¶
Operational access for event generation and monitoring.
Permissions:
- ✅ View event headers
- ✅ Expand and view event details
- ✅ Access generator panel
- ✅ Generate single events (iterations locked to 1)
- ❌ Cannot change iterations (locked to 1)
- ❌ Cannot change delay (locked to 150ms)
User Role¶
Read-only access to view incoming events.
Permissions:
- ✅ View event headers (collapsed)
- ❌ Cannot expand event details
- ❌ Cannot access generator panel
- ❌ Cannot generate events
Role-Based Access Control¶
Backend Authorization¶
The API enforces role-based access at the endpoint level:
Generator Endpoint (/api/generate)¶
- Required Role:
operatororadmin - Admin-Only Features:
iterations > 1: Only admins can generate multiple eventsdelay != 150: Only admins can customize delay between events
Example Response (403 Forbidden):
Frontend Authorization¶
The UI automatically adapts based on user permissions:
For Users (Basic Role)¶
- Generator link hidden from navigation
- Keyboard shortcuts disabled (Ctrl/Meta+Up)
- Event accordion buttons disabled
- "Expand all" button hidden
- Can only view collapsed event headers
For Operators¶
- Generator accessible in navigation and via keyboard
- Iterations slider disabled (locked to 1)
- Delay slider disabled (locked to 150ms)
- Can expand events to view full details
- Form validates permissions before submission
For Admins¶
- Full access to all UI features
- All controls enabled
- Can adjust iterations (1-100)
- Can adjust delay (1-2000ms)
Keycloak Configuration¶
Setting Up Keycloak¶
- Start Keycloak (example using Docker):
docker run -d \
--name keycloak \
-p 8090:8080 \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:22.0.5 \
start-dev
-
Create a Realm
-
Login to Keycloak admin console: http://localhost:8090
-
Create a new realm named
events-player -
Create Realm Roles
Create three realm roles:
admin- Full administrative accessoperator- Operational access-
user- Read-only access -
Create Client for Frontend
Create a public client for the web UI:
- Client ID:
events-player-web - Client Type: OpenID Connect
- Access Type: Public
- Valid Redirect URIs:
http://localhost:8884/* - Web Origins:
http://localhost:8884 - Standard Flow Enabled: ON
- Direct Access Grants Enabled: ON
- PKCE Code Challenge Method: S256
Important: No additional client configuration is needed for token refresh. The application automatically requests the offline_access scope during OAuth login, which instructs Keycloak to issue refresh tokens. This is standard OIDC behavior.
- Create Client for Backend (optional)
Create a bearer-only client for JWT validation:
- Client ID:
events-player-api - Client Type: OpenID Connect
- Access Type: Bearer-only
-
Standard Flow Enabled: OFF
-
Create Users
Create test users and assign roles:
Username: admin
Password: admin123
Roles: admin
Username: operator
Password: operator123
Roles: operator
Username: user
Password: user123
Roles: user
Environment Variables for Keycloak¶
API_AUTH_MODE¶
- Description: Authentication mode
- Type: String
- Default:
"none" - Valid Values:
none,keycloak,istio,auto - Example:
API_AUTH_MODE=auto
API_AUTH_JWKS_URL¶
- Description: JWKS endpoint URL for JWT validation
- Type: String (URL)
- Required: When auth_mode is
istioorauto - Example:
API_AUTH_JWKS_URL=http://keycloak:8080/realms/events-player/protocol/openid-connect/certs
The JWKS (JSON Web Key Set) endpoint provides public keys for JWT signature verification.
API_AUTH_ISSUER¶
- Description: Expected JWT issuer (iss claim)
- Type: String
- Required: When auth_mode is
istioorauto - Example:
API_AUTH_ISSUER=http://localhost:8090/realms/events-player
Must match the issuer in JWT tokens. Use the external URL accessible from browsers.
API_AUTH_AUDIENCE¶
- Description: Expected JWT audience (aud claim)
- Type: String
- Required: When auth_mode is
istioorauto - Example:
API_AUTH_AUDIENCE=events-player-web
The intended audience for the JWT token, typically the client ID.
API_AUTH_REQUIRED¶
- Description: Whether authentication is required for all endpoints
- Type: Boolean (string)
- Default:
"false" - Example:
API_AUTH_REQUIRED=false
When false, authentication is optional. Protected endpoints still require authentication, but public endpoints remain accessible.
API_AUTH_TRUST_MODE¶
- Description: Skip JWT signature verification (trust mode)
- Type: Boolean (string)
- Default:
"false" - Example:
API_AUTH_TRUST_MODE=true
⚠️ Use with caution: When enabled, the application will decode JWT tokens without verifying:
- Signature (no JWKS lookup required)
- Issuer validation
- Audience validation
When to use:
- Running behind Istio/service mesh with RequestAuthentication policy
- Proxy layer (e.g., Istio) has already validated the JWT
- Token issuer/realm differs from your configured OAuth settings
- Troubleshooting key ID mismatch issues
Security implications:
- Only enable in trusted environments where JWT validation is handled upstream
- The application still extracts user info and enforces RBAC
- Ensure your proxy/mesh is properly configured to validate tokens
Example with Istio:
docker run -p 8080:8080 \
-e AUTH_REQUIRED=true \
-e AUTH_TRUST_MODE=true \
ghcr.io/bvandewe/events-player:latest
API_KEYCLOAK_URL¶
- Description: Internal Keycloak URL (for backend)
- Type: String (URL)
- Required: When auth_mode is
keycloakorauto - Example:
API_KEYCLOAK_URL=http://keycloak:8080
Used by backend for token exchange. Can be internal Docker hostname.
API_KEYCLOAK_URL_EXTERNAL¶
- Description: External Keycloak URL (for frontend)
- Type: String (URL)
- Required: When auth_mode is
keycloakorauto - Example:
API_KEYCLOAK_URL_EXTERNAL=http://localhost:8090
URL accessible from browsers for OAuth redirects. Use localhost or public hostname.
API_KEYCLOAK_REALM¶
- Description: Keycloak realm name
- Type: String
- Default:
"events-player" - Example:
API_KEYCLOAK_REALM=events-player
API_KEYCLOAK_CLIENT_ID¶
- Description: Keycloak client ID for the web application
- Type: String
- Default:
"events-player-web" - Example:
API_KEYCLOAK_CLIENT_ID=events-player-web
Must be a public client configured with PKCE support.
API_KEYCLOAK_CLIENT_SECRET¶
- Description: Client secret (leave empty for public clients)
- Type: String
- Default:
"" - Example:
API_KEYCLOAK_CLIENT_SECRET=
Public clients using PKCE don't require a client secret.
Docker Compose Example¶
Here's a complete docker-compose setup with Keycloak authentication:
version: "3.8"
services:
keycloak:
image: quay.io/keycloak/keycloak:22.0.5
container_name: keycloak
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
KC_HOSTNAME: localhost
KC_HOSTNAME_PORT: 8090
KC_HOSTNAME_STRICT: false
KC_HTTP_ENABLED: true
KC_HOSTNAME_STRICT_HTTPS: false
ports:
- "8090:8080"
volumes:
- ./deployments/keycloak/events-player-realm-export.json:/opt/keycloak/data/import/events-player-realm-export.json
command:
- start-dev
- --import-realm
events-player:
image: ghcr.io/bvandewe/events-player:latest
container_name: events-player
ports:
- "8884:8080"
environment:
# Authentication configuration
api_auth_mode: auto
api_auth_jwks_url: http://keycloak:8080/realms/events-player/protocol/openid-connect/certs
api_auth_issuer: http://localhost:8090/realms/events-player
api_auth_audience: events-player-web
api_auth_required: "false"
# Keycloak OAuth configuration
api_keycloak_url: http://keycloak:8080
api_keycloak_url_external: http://localhost:8090
api_keycloak_realm: events-player
api_keycloak_client_id: events-player-web
api_keycloak_client_secret: ""
depends_on:
- keycloak
Testing Authentication¶
Test as Admin¶
- Navigate to http://localhost:8884
- Click the "Login" button
- Login with:
- Username:
admin - Password:
admin123 - Verify:
- ✅ Generator link visible
- ✅ All controls enabled
- ✅ Can adjust iterations and delay
- ✅ Can expand events
Test as Operator¶
- Logout (if logged in)
- Login with:
- Username:
operator - Password:
operator123 - Verify:
- ✅ Generator link visible
- ❌ Iterations slider disabled (locked to 1)
- ❌ Delay slider disabled (locked to 150ms)
- ✅ Can expand events
Test as User¶
- Logout (if logged in)
- Login with:
- Username:
user - Password:
user123 - Verify:
- ❌ Generator link hidden
- ❌ Cannot expand events
- ✅ Can view event headers
Security Features¶
PKCE (Proof Key for Code Exchange)¶
CloudEvent Player implements OAuth 2.0 with PKCE for enhanced security:
- Generates
code_verifier(random 43-128 character string) - Creates
code_challengeusing SHA-256 hash - Sends
code_challengeduring authorization - Sends
code_verifierduring token exchange - Keycloak validates that hash(code_verifier) == code_challenge
JWT Validation¶
Backend validates all JWT tokens:
- Signature Verification: RS256 with public key from JWKS
- Expiration Check: Rejects expired tokens
- Issuer Validation: Verifies token issuer matches configuration
- Audience Validation: Ensures token is intended for this application
- JWKS Caching: Public keys cached for 5 minutes
Defense in Depth¶
Authorization is enforced at multiple layers:
- Backend API: Role checks on protected endpoints
- Frontend UI: Disabled controls and hidden elements
- Form Validation: Client-side checks before submission
- HTTP Headers: Authorization header with Bearer token
Troubleshooting¶
Login Redirect Issues¶
Problem: After login, redirected to wrong URL
Solution: Check api_keycloak_url_external matches the URL in browser
# Should be browser-accessible URL, not Docker internal hostname
api_keycloak_url_external=http://localhost:8090 # ✅ Correct
api_keycloak_url_external=http://keycloak:8080 # ❌ Wrong (internal)
Bearer-Only Error¶
Problem: "Bearer-only applications are not allowed to initiate browser login"
Solution: Ensure client is configured as public, not confidential or bearer-only
{
"clientId": "events-player-web",
"publicClient": true, // Must be true
"bearerOnly": false // Must be false
}
PKCE Code Verifier Error¶
Problem: "PKCE code verifier not specified"
Solution: Clear browser sessionStorage and try again
Roles Not Appearing¶
Problem: User roles not showing in JWT
Solution: Add protocol mapper in Keycloak client:
- Go to client → Client Scopes → Dedicated scope
- Add mapper → By configuration → User Realm Role
- Token Claim Name:
realm_access.roles - Claim JSON Type: String
- Add to access token: ON
CORS Errors¶
Problem: Cross-origin request blocked
Solution: Check Web Origins in Keycloak client configuration
Token Refresh Not Working¶
Problem: Automatic token refresh fails, user forced to log in again
Solution: Ensure the OAuth flow requests the offline_access scope
The application automatically requests this scope during login. If you've customized the OAuth flow, verify:
// In auth.js - OAuth authorization request
scope: "openid profile email offline_access"; // offline_access required
Keycloak Configuration: No special client configuration needed. The offline_access scope is a standard OIDC scope that instructs Keycloak to issue refresh tokens.
Debugging:
- Open browser DevTools → Application → Session Storage
- Check for
refresh_tokenkey after login - If missing, verify the OAuth scope parameter includes
offline_access
Token Management¶
Automatic Token Refresh¶
CloudEvent Player implements automatic OIDC token refresh to prevent user session interruptions:
How It Works¶
- Proactive Refresh: Tokens are automatically refreshed 5 minutes before expiry
- Background Monitoring: System checks token expiry every 60 seconds
- Seamless Updates: Refresh happens in background without user interaction
- 401 Retry: API calls that fail with 401 automatically retry after refresh
Token Storage¶
Tokens are stored in browser sessionStorage:
access_token: Current JWT access tokenrefresh_token: Token used to obtain new access tokenstoken_expires_at: Timestamp for proactive refresh calculation
Refresh Flow¶
- System detects token will expire within 5 minutes
- Calls
/api/auth/refreshendpoint with refresh token - Backend exchanges refresh token with Keycloak for new access token
- New tokens stored in sessionStorage
- User session continues uninterrupted
Manual Token Refresh¶
Tokens also refresh automatically when:
- Any API call receives a 401 response
- User attempts an operation requiring authentication
- Token validation detects expiry
Session Expiry¶
If token refresh fails (e.g., refresh token expired or revoked):
- User sees notification about session expiry
- Application switches to read-only mode
- User prompted to log in again
- All stored tokens cleared from sessionStorage
Token Validation¶
The application validates tokens on every API request:
- Checks token presence in sessionStorage
- Verifies token expiry timestamp
- Automatically refreshes if expired or expiring soon
- Falls back to login if refresh fails
API Endpoints¶
Public Endpoints¶
GET /- Web UI (no auth required)GET /api/health- Health checkGET /api/auth/info- Authentication status
Protected Endpoints¶
Event Generation¶
POST /api/generate- Generate events (requiresoperatororadmin)- Submits background task for event generation
- Returns task ID for tracking
- Validates OAuth token before processing
Task Management¶
-
GET /api/tasks- View all active tasks (requiresadmin) -
Lists running, pending, completed, and failed tasks
- Returns task status, progress, and timestamps
-
Admin-only endpoint
-
POST /api/task/{task_id}/cancel- Cancel specific task (requiresadmin) -
Gracefully stops event generation task
- Events generated before cancellation are preserved
-
Admin-only endpoint
-
POST /api/tasks/cancel-all- Cancel all running tasks (requiresadmin) - Bulk cancellation for all active tasks
- Emergency control feature
- Admin-only endpoint
Authentication Management¶
-
GET /api/auth/info- Get current authentication status and config -
Returns user info if authenticated
- Provides Keycloak configuration for login
-
Public endpoint
-
POST /api/auth/callback- OAuth callback handler for token exchange -
Exchanges authorization code for tokens
- PKCE verification
-
Returns access_token, refresh_token, and user info
-
POST /api/auth/refresh- Refresh access token - Exchanges refresh_token for new access_token
- Extends user session without re-login
- Returns new tokens and updated expiry
Authorization Rules¶
| Endpoint | Anonymous | User | Operator | Admin |
|---|---|---|---|---|
GET / |
✅ | ✅ | ✅ | ✅ |
GET /api/health |
✅ | ✅ | ✅ | ✅ |
GET /api/auth/info |
✅ | ✅ | ✅ | ✅ |
POST /api/auth/callback |
✅ | ✅ | ✅ | ✅ |
POST /api/auth/refresh |
✅ | ✅ | ✅ | ✅ |
POST /api/generate |
❌ | ❌ | ✅ | ✅ |
GET /api/tasks |
❌ | ❌ | ❌ | ✅ |
POST /api/task/*/cancel |
❌ | ❌ | ❌ | ✅ |
POST /api/tasks/cancel-all |
❌ | ❌ | ❌ | ✅ |
Mode Detection Internals¶
How Frontend Detects Authentication Mode¶
The frontend automatically determines the authentication mode by examining the /api/auth/info response:
// Frontend logic (src/ui/js/auth/auth.js)
const response = await fetch('/api/auth/info');
const data = await response.json();
if (data.authenticated) {
// User is already authenticated → Istio/Proxy mode
this.mode = 'istio';
this.userInfo = data.user;
} else if (data.oauth_config) {
// OAuth config provided → OAuth mode
this.mode = 'oauth';
this.oauthConfig = data.oauth_config;
} else {
// No authentication → Open mode
this.mode = 'none';
}
Key Insight: The frontend does NOT use the mode field from the backend response. Instead, it independently determines the mode based on authentication state. This makes the system resilient to backend configuration issues.
Authentication State Logic¶
┌────────────────────────────────────────┐
│ Frontend: GET /api/auth/info │
└──────────────┬─────────────────────────┘
│
▼
┌──────────────────────┐
│ authenticated: true? │
└──────┬───────────────┘
│
┌────┴────┐
│ │
▼ ▼
YES NO
│ │
│ └──> Check oauth_config
│ │
│ ┌─────┴─────┐
│ │ │
│ YES NO
│ │ │
▼ ▼ ▼
mode='istio' mode='oauth' mode='none'
Why This Design is Resilient¶
- Works with minimal configuration: Backend can have no
AUTH_JWKS_URLorOAUTH_SERVER_URLand still function correctly if user is authenticated - Adapts to deployment: Automatically handles OAuth2 Proxy, Istio, or direct OAuth scenarios
- Frontend independence: Frontend doesn't depend on backend environment variables
- Simple logic: Only three states to handle based on actual authentication state
Example: OAuth2 Proxy + Istio¶
In production with OAuth2 Proxy:
- OAuth2 Proxy intercepts browser request
- OAuth2 Proxy handles OAuth flow with Keycloak
- OAuth2 Proxy injects JWT in request header
- Backend extracts JWT, validates user
- Backend returns
/api/auth/infowithauthenticated: true - Frontend sees
authenticated: true→ Setsmode = 'istio' - No login button shown, user already authenticated ✅
Backend configuration can be minimal:
See notes/MODE_DETECTION_CASE_STUDY.md for the full technical analysis.
Next Steps¶
- Configuration Guide - Full list of environment variables
- Deployment Guide - Deploy with authentication
- Quick Start - Get started with CloudEvent Player