# 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 schema - `src/database/db.ts` - Database connection using Drizzle ORM - `drizzle.config.ts` - Drizzle configuration **User Schema:** ```typescript { 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 Keycloak - `extractToken(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 context - `requireAuth` - Helper function to require authentication **Usage:** ```typescript 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 payload - `getUserByKeycloakId(keycloakId)` - Retrieves user by Keycloak ID ### Frontend (Next.js) #### 1. Keycloak Client (`src/lib/keycloak-client.ts`) **Functions:** - `initKeycloak()` - Initializes Keycloak with `login-required` mode - `logout()` - Logs out user and clears tokens - `getUserInfo()` - Returns parsed token payload - `getToken()` - Returns current access token - `isAuthenticated()` - 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:** ```typescript { isAuthenticated: boolean; isLoading: boolean; userInfo: any; logout: () => Promise; } ``` **Hook:** - `useAuth()` - Access auth context in components #### 3. API Client (`src/lib/api-client.ts`) **Enhanced Features:** - Automatically adds `Authorization: Bearer ` header - Handles 401 errors by triggering token refresh - Reads token from `window.__KEYCLOAK_TOKEN__` ## Setup Instructions ### 1. Database Setup ```bash # 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: 1. Log in to Keycloak Admin Console 2. Create a new realm (e.g., `allaos`) 3. 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` #### Configure Environment Variables: ```env # 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 ```bash npm install keycloak jose npm install -D @types/keycloak-js ``` ### 4. Run Application ```bash npm run dev ``` ## Usage Examples ### Protecting API Routes ```typescript 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 ```typescript "use client"; import { useAuth } from "@/providers/AuthProvider"; export default function UserProfile() { const { isAuthenticated, isLoading, userInfo, logout } = useAuth(); if (isLoading) return
Loading...
; if (!isAuthenticated) return
Not authenticated
; return (

Welcome, {userInfo?.name}

Email: {userInfo?.email}

); } ``` ### Making Authenticated API Calls ```typescript import { apiClient } from "@/lib/api-client"; // Automatically includes Bearer token const data = await apiClient("/api/protected-endpoint"); ``` ## Token Flow ### 1. Initialization 1. User visits application 2. `AuthProvider` initializes Keycloak 3. Keycloak redirects to login page (if not authenticated) 4. User logs in 5. Keycloak redirects back with code 6. Keycloak exchanges code for tokens 7. Access token stored in memory (`window.__KEYCLOAK_TOKEN__`) ### 2. API Calls 1. Component calls `apiClient()` 2. API client reads token from `window.__KEYCLOAK_TOKEN__` 3. Adds `Authorization: Bearer ` header 4. Backend receives request, extracts token 5. Verifies token using JWKS 6. Finds/creates user in database 7. Attaches user to request context 8. Route handler processes request ### 3. Token Refresh 1. Background interval checks token every second 2. If token expires in < 30 seconds, refresh automatically 3. If refresh fails, redirect to login 4. 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 1. **Enable HTTPS in production** 2. **Set up Keycloak SSL** 3. **Implement rate limiting** on auth endpoints 4. **Add session timeout** on client side 5. **Implement CSRF protection** for state-changing operations 6. **Add audit logging** for authentication events 7. **Enable Keycloak events** for security monitoring ## Testing ### Manual Testing 1. Start Keycloak and your application 2. Visit `http://localhost:3000` 3. You should be redirected to Keycloak login 4. Login with test credentials 5. After login, you should see the application 6. Open browser DevTools → Network 7. Check that API calls have `Authorization: Bearer ` header ### Testing Token Expiry 1. Set Keycloak token expiry to 1 minute (for testing) 2. Login to application 3. Wait for token to expire 4. Try making an API call 5. Token should refresh automatically 6. If refresh fails, should redirect to login ### Testing Invalid Token 1. Manually modify `window.__KEYCLOAK_TOKEN__` in DevTools 2. Make an API call 3. Should receive 401 error 4. 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_URL` in .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 ## References - [Keycloak Documentation](https://www.keycloak.org/documentation) - [OpenID Connect Core](https://openid.net/connect/) - [ElysiaJS Documentation](https://elysiajs.com/) - [Drizzle ORM Documentation](https://orm.drizzle.team/) - [Next.js Documentation](https://nextjs.org/docs)