12 KiB
12 KiB
Keycloak Authentication Implementation
Overview
This document describes the Keycloak (OIDC) authentication implementation integrated into the Next.js + ElysiaJS application.
Architecture
Authentication Flow
┌─────────────┐ 1. Init ┌──────────────┐ 2. Login ┌──────────┐
│ Browser │ ──────────────> │ Keycloak │ ──────────────> │ Browser │
│ │ │ Server │ │ │
└─────────────┘ └──────────────┘ └──────────┘
│ │
│ 3. Token (JWT) │
├──────────────────────────────────────────────────────────────>│
│ │
│ 4. API Call with Bearer Token │
├─────────────────────────────────────────────┐ │
│ │ │
▼ ▼ │
┌─────────────┐ 5. Verify Token ┌──────────────┐ │
│ Next.js API │ ───────────────────> │ Database │ │
│ (Elysia) │ │ (PostgreSQL) │ │
└─────────────┘ └──────────────┘ │
│ │
│ 6. User Context │
├──────────────────────────────────────────────────────────────────>│
Components
Backend (ElysiaJS)
1. Database Layer (src/database/)
Files:
src/database/schema/users.ts- User table schemasrc/database/db.ts- Database connection using Drizzle ORMdrizzle.config.ts- Drizzle configuration
User Schema:
{
id: uuid (primary key)
keycloakId: text (unique, from Keycloak sub)
email: text
name: text
createdAt: timestamp
updatedAt: timestamp
}
2. Keycloak Verification (src/lib/keycloak.ts)
Functions:
verifyToken(token)- Verifies JWT using JWKS from KeycloakextractToken(authHeader)- Extracts Bearer token from Authorization header
Features:
- Automatic JWKS caching
- Token validation (issuer, audience, expiration)
- Type-safe token payload
3. Auth Middleware (src/middleware/auth.ts)
Exports:
authPlugin- Elysia plugin that validates tokens and attaches user to contextrequireAuth- Helper function to require authentication
Usage:
import { authPlugin } from "@/middleware/auth";
// Apply to all routes
const app = new Elysia().use(authPlugin).get("/protected", ({ user }) => {
return { message: "Hello!", user };
});
4. User Service (src/modules/auth/service.ts)
Functions:
findOrCreateUser(payload)- Finds existing user or creates new one from Keycloak payloadgetUserByKeycloakId(keycloakId)- Retrieves user by Keycloak ID
Frontend (Next.js)
1. Keycloak Client (src/lib/keycloak-client.ts)
Functions:
initKeycloak()- Initializes Keycloak withlogin-requiredmodelogout()- Logs out user and clears tokensgetUserInfo()- Returns parsed token payloadgetToken()- Returns current access tokenisAuthenticated()- Check if user is authenticated
Features:
- Memory-only token storage (no localStorage)
- Automatic token refresh (30 seconds before expiry)
- Token refresh on 401 errors
- PKCE flow for security
2. Auth Provider (src/providers/AuthProvider.tsx)
Context:
{
isAuthenticated: boolean;
isLoading: boolean;
userInfo: any;
logout: () => Promise<void>;
}
Hook:
useAuth()- Access auth context in components
3. API Client (src/lib/api-client.ts)
Enhanced Features:
- Automatically adds
Authorization: Bearer <token>header - Handles 401 errors by triggering token refresh
- Reads token from
window.__KEYCLOAK_TOKEN__
Setup Instructions
1. Database Setup
# Copy environment template
cp .env.example .env
# Edit .env with your database credentials
# DATABASE_URL=postgresql://user:password@localhost:5432/allaos
# Generate and run migration
npx drizzle-kit generate
npx drizzle-kit migrate
2. Keycloak Setup
Create a Realm and Client:
- Log in to Keycloak Admin Console
- Create a new realm (e.g.,
allaos) - Create a new OpenID Connect client:
- Client ID:
allaos-frontend - Client Authentication:
ON(for backend) - Valid Redirect URIs:
http://localhost:3000/* - Web Origins:
http://localhost:3000 - Access Type:
confidential
- Client ID:
Configure Environment Variables:
# Backend (.env)
KEYCLOAK_URL=http://localhost:8080
KEYCLOAK_REALM=allaos
KEYCLOAK_CLIENT_ID=allaos-frontend
KEYCLOAK_CLIENT_SECRET=your-client-secret
# Frontend (.env.local or NEXT_PUBLIC_ in .env)
NEXT_PUBLIC_KEYCLOAK_URL=http://localhost:8080
NEXT_PUBLIC_KEYCLOAK_REALM=allaos
NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=allaos-frontend
3. Install Dependencies
npm install keycloak jose
npm install -D @types/keycloak-js
4. Run Application
npm run dev
Usage Examples
Protecting API Routes
import { Elysia } from "elysia";
import { authPlugin } from "@/middleware/auth";
const app = new Elysia({ prefix: "/api" })
.use(authPlugin)
.get("/protected", ({ user, tokenPayload }) => {
// user is now available from database
// tokenPayload contains Keycloak claims
return {
message: "Protected data",
user: {
id: user.id,
email: user.email,
name: user.name,
},
};
});
Accessing User Info in Components
"use client";
import { useAuth } from "@/providers/AuthProvider";
export default function UserProfile() {
const { isAuthenticated, isLoading, userInfo, logout } = useAuth();
if (isLoading) return <div>Loading...</div>;
if (!isAuthenticated) return <div>Not authenticated</div>;
return (
<div>
<h1>Welcome, {userInfo?.name}</h1>
<p>Email: {userInfo?.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
Making Authenticated API Calls
import { apiClient } from "@/lib/api-client";
// Automatically includes Bearer token
const data = await apiClient("/api/protected-endpoint");
Token Flow
1. Initialization
- User visits application
AuthProviderinitializes Keycloak- Keycloak redirects to login page (if not authenticated)
- User logs in
- Keycloak redirects back with code
- Keycloak exchanges code for tokens
- Access token stored in memory (
window.__KEYCLOAK_TOKEN__)
2. API Calls
- Component calls
apiClient() - API client reads token from
window.__KEYCLOAK_TOKEN__ - Adds
Authorization: Bearer <token>header - Backend receives request, extracts token
- Verifies token using JWKS
- Finds/creates user in database
- Attaches user to request context
- Route handler processes request
3. Token Refresh
- Background interval checks token every second
- If token expires in < 30 seconds, refresh automatically
- If refresh fails, redirect to login
- On 401 error, trigger immediate refresh attempt
Security Considerations
✅ Implemented
- Memory-only token storage - No localStorage/sessionStorage
- PKCE flow - Prevents authorization code interception
- JWT verification - Using JWKS from Keycloak
- Token expiration - Automatic refresh before expiry
- HTTPS ready - Works with secure cookies and headers
- CORS configured - Only allowed origins
⚠️ Additional Recommendations
- Enable HTTPS in production
- Set up Keycloak SSL
- Implement rate limiting on auth endpoints
- Add session timeout on client side
- Implement CSRF protection for state-changing operations
- Add audit logging for authentication events
- Enable Keycloak events for security monitoring
Testing
Manual Testing
- Start Keycloak and your application
- Visit
http://localhost:3000 - You should be redirected to Keycloak login
- Login with test credentials
- After login, you should see the application
- Open browser DevTools → Network
- Check that API calls have
Authorization: Bearer <token>header
Testing Token Expiry
- Set Keycloak token expiry to 1 minute (for testing)
- Login to application
- Wait for token to expire
- Try making an API call
- Token should refresh automatically
- If refresh fails, should redirect to login
Testing Invalid Token
- Manually modify
window.__KEYCLOAK_TOKEN__in DevTools - Make an API call
- Should receive 401 error
- Should trigger token refresh
Troubleshooting
Issue: "Unauthorized: Invalid or expired token"
Possible causes:
- Token expired and refresh failed
- Keycloak URL/realm/client ID mismatch
- JWKS endpoint unreachable
Solutions:
- Check environment variables
- Verify Keycloak is running
- Check browser console for errors
- Verify JWKS endpoint is accessible
Issue: User not created in database
Possible causes:
- Database connection failed
- Migration not run
- Database permissions issue
Solutions:
- Run
npx drizzle-kit migrate - Check
DATABASE_URLin .env - Verify database is accessible
Issue: Redirect loop
Possible causes:
- Keycloak callback URL not configured
- Client not created or disabled
- Invalid redirect URI
Solutions:
- Check Keycloak client settings
- Verify Valid Redirect URIs
- Check client is enabled
File Structure
src/
├── database/
│ ├── db.ts # Database connection
│ └── schema/
│ ├── users.ts # User schema
│ └── index.ts # Schema exports
├── lib/
│ ├── keycloak.ts # JWT verification
│ ├── keycloak-client.ts # Keycloak JS client
│ └── api-client.ts # API client with auth
├── middleware/
│ └── auth.ts # Elysia auth plugin
├── modules/
│ └── auth/
│ └── service.ts # User sync logic
├── providers/
│ └── AuthProvider.tsx # React auth context
└── app/
└── layout.tsx # Root layout with AuthProvider
Next Steps (Phase 2 & 3)
Phase 2: Role-Based Access Control (RBAC)
- Store user roles in database
- Add role claims to token verification
- Create role-based route protection
- Add admin/role management UI
Phase 3: Multi-Tenant Support
- Add tenant_id to user schema
- Filter data by tenant
- Add tenant context to requests
- Implement tenant isolation