423 lines
12 KiB
Markdown
423 lines
12 KiB
Markdown
# 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<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
|
|
|
|
```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 <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
|
|
|
|
```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 <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
|
|
|
|
- [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)
|