Files
nextjs-elysia-allaos/KEYCLOAK_AUTH.md
phaichayon a330abf9b6 commit
2026-04-23 15:37:01 +07:00

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 schema
  • src/database/db.ts - Database connection using Drizzle ORM
  • drizzle.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 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:

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:

{
  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:

  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:

# 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

  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 <token> 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 <token> 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