commit
This commit is contained in:
422
KEYCLOAK_AUTH.md
Normal file
422
KEYCLOAK_AUTH.md
Normal file
@@ -0,0 +1,422 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user