A complete authentication solution using Keycloak with PKCE (Proof Key for Code Exchange) flow, featuring a React frontend and FastAPI backend.
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ React │ │ Keycloak │ │ FastAPI │
│ Frontend │◄───────►│ Server │ │ Backend │
│ (Port 5173) │ HTTPS │ + Nginx SSL │ │ (Port 8000) │
└─────────────┘ └──────────────┘ └─────────────┘
(your-keycloak-domain.com) ▲
│
JWT Token
Validation
sequenceDiagram
participant U as User
participant F as Frontend (React)
participant K as Keycloak
participant B as Backend (FastAPI)
Note over F: 1. User clicks "Sign In"
F->>F: Generate code_verifier (random)
F->>F: Generate code_challenge = SHA256(code_verifier)
Note over F,K: 2. Authorization Request
F->>K: Authorization Request<br/>(code_challenge, code_challenge_method=S256)
K->>U: Show Login Page
U->>K: Enter credentials
Note over K,F: 3. Authorization Response
K->>F: Authorization Code (redirect to /auth/callback)
Note over F,K: 4. Token Exchange
F->>K: Token Request<br/>(authorization_code + code_verifier)
K->>K: Verify code_verifier matches code_challenge
K->>F: Access Token + ID Token + Refresh Token
Note over F: 5. Store tokens & user info
F->>F: Save tokens in memory
F->>U: Show authenticated UI
Note over F,B: 6. API Request
U->>F: Click "Test API"
F->>B: API Request<br/>Authorization: Bearer {access_token}
B->>K: Verify token (fetch JWKS public keys)
K->>B: Public keys
B->>B: Validate JWT signature & claims
B->>F: Protected data
F->>U: Display result
Note over F,K: 7. Logout
U->>F: Click "Sign Out"
F->>K: Logout Request
K->>K: Invalidate session
K->>F: Redirect to /auth/logout
F->>U: Show login screen
-
User Action: User clicks "Sign In with Keycloak"
-
Code Challenge Generation (Frontend):
code_verifier = random_string(128 chars) code_challenge = BASE64URL(SHA256(code_verifier)) -
Authorization Request (Frontend → Keycloak):
GET https://naic-kc.ashen.no/realms/naic-monitor/protocol/openid-connect/auth Parameters: - client_id: naic-monitor-client - redirect_uri: http://localhost:5173/auth/callback - response_type: code - scope: openid profile email - code_challenge: {generated_challenge} - code_challenge_method: S256 -
User Authentication (Keycloak):
- User enters username/password
- Keycloak validates credentials
- Keycloak stores code_challenge
-
Authorization Code (Keycloak → Frontend):
Redirect: http://localhost:5173/auth/callback?code={auth_code} -
Token Exchange (Frontend → Keycloak):
POST https://your-keycloak-domain.com/realms/your-realm/protocol/openid-connect/token Body: - grant_type: authorization_code - code: {auth_code} - redirect_uri: http://localhost:5173/auth/callback - client_id: your-client-id - code_verifier: {original_verifier} -
Token Response (Keycloak → Frontend):
{ "access_token": "eyJhbGci...", "token_type": "Bearer", "expires_in": 300, "refresh_token": "eyJhbGci...", "id_token": "eyJhbGci..." }
-
Frontend stores access token in memory (AuthContext)
-
API Call (Frontend → Backend):
GET /api/protected Headers: - Authorization: Bearer {access_token} -
Token Validation (Backend):
- Extract token from Authorization header
- Fetch Keycloak's public keys (JWKS)
- Verify JWT signature using RS256
- Validate claims:
iss(issuer): https://your-keycloak-domain.com/realms/your-realmaud(audience): accountexp(expiration): not expirediat(issued at): valid time
-
Response (Backend → Frontend):
{ "message": "Protected data", "user": { "sub": "user-id", "email": "user@example.com" } }
-
Token Expiration Check (Frontend):
- oidc-client-ts monitors token expiration
- Triggers silent renewal 60 seconds before expiry
-
Silent Refresh (Frontend → Keycloak):
POST https://your-keycloak-domain.com/realms/your-realm/protocol/openid-connect/token Body: - grant_type: refresh_token - refresh_token: [refresh_token] - client_id: your-client-id -
New Tokens (Keycloak → Frontend):
- New access_token
- New refresh_token
- Updated in AuthContext
-
User Action: User clicks "Sign Out"
-
Logout Request (Frontend → Keycloak):
GET https://your-keycloak-domain.com/realms/your-realm/protocol/openid-connect/logout Parameters: - id_token_hint: {id_token} - post_logout_redirect_uri: http://localhost:5173/auth/logout -
Session Cleanup (Keycloak):
- Invalidate session
- Clear cookies
-
Redirect (Keycloak → Frontend):
- Redirect to /auth/logout
- Frontend clears tokens
- Show login screen
- Algorithm: RS256 (asymmetric encryption)
- Public Key: Fetched from Keycloak JWKS endpoint
- Claims Verified:
- Signature (cryptographic validation)
- Issuer (prevents token from other sources)
- Audience (ensures token is for this API)
- Expiration (prevents old token reuse)
- Issued At (prevents pre-dated tokens)
- Backend only accepts requests from
http://localhost:5173 - Credentials included in requests
- Specific methods and headers allowed
- Access Token: Stored in memory (AuthContext)
- Not in localStorage: Prevents XSS attacks
- Auto-refresh: Silent renewal before expiration
- Node.js 18+
- Python 3.9+
- Keycloak server running with HTTPS (see docker-keycloak/ReadMe.md)
- SSL certificates configured (see docker-keycloak/SSL_SETUP.md)
cd frontend
npm install
npm run devAccess at: http://localhost:5173
cd backend
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python main.pyAPI available at: http://localhost:8000 API Docs: http://localhost:8000/docs
Client Settings (your-client-id):
- Client Protocol: openid-connect
- Access Type: Public
- Standard Flow: ON
- Valid Redirect URIs:
http://localhost:5173/auth/callbackhttp://localhost:5173/*
- Valid Post Logout Redirect URIs:
http://localhost:5173/auth/logouthttp://localhost:5173/
- Web Origins:
http://localhost:5173
.
├── frontend/ # React application
│ ├── src/
│ │ ├── auth/
│ │ │ ├── authConfig.ts # OIDC configuration
│ │ │ ├── AuthContext.tsx # Auth provider & hooks
│ │ │ ├── AuthCallback.tsx # OAuth callback handler
│ │ │ └── LogoutCallback.tsx # Logout handler
│ │ ├── components/
│ │ │ ├── ApiTestPage.tsx # API testing interface
│ │ │ └── ui/ # Chakra UI components
│ │ ├── services/
│ │ │ └── api.ts # API client with axios & auth
│ │ ├── App.tsx # Main app component
│ │ └── main.tsx # App entry point
│ └── package.json
│
├── backend/ # FastAPI application
│ ├── main.py # API server with JWT validation
│ ├── requirements.txt # Python dependencies
│ └── README.md # Backend documentation
│
└── README.md # This file
GET /- Welcome messageGET /health- Health check
GET /api/protected- Basic protected routeGET /api/user/profile- Current user profileGET /api/user/roles- User roles and permissionsPOST /api/data- Create data (example)GET /api/admin/users- Admin only (requires 'admin' role)
-
Start both servers:
- Frontend: http://localhost:5173
- Backend: http://localhost:8000
-
Login:
- Click "Sign In with Keycloak"
- Enter credentials in Keycloak
- Redirected back with tokens
-
Test API:
- Click "Test API" button
- Try different endpoints
- Observe token in Authorization header (axios automatically adds it)
-
Logout:
- Click "Sign Out"
- Session cleared in Keycloak
- Tokens removed from frontend