Authentication System Overview
Akako LMS uses Clerk.js for authentication with PostgreSQL-based role management. This modern approach separates authentication (handled by Clerk) from authorization (handled by our database), providing flexibility and maintainability.
Architecture
Authentication Flow
graph TD
A[User Registration/Login] --> B[Clerk.js Authentication]
B --> C[JWT Token with Clerk ID]
C --> D[Server-side Role Validation]
D --> E[PostgreSQL Role Lookup]
E --> F[Access Granted/Denied]
Key Components
- Clerk.js: Handles user authentication, session management, and JWT tokens
- PostgreSQL: Stores user data and role assignments with effective date management
- Custom JWT System: Embeds role information in tokens for efficient authorization
- Role Guard Middleware: Server-side validation for API endpoints
Role System
Available Roles
- LEARNER: Default role for all users, provides access to courses and learning materials
- MENTOR: Can create and manage educational content, guide students
- ADMIN: Full system access, user management, and administrative functions
Role Assignment
Roles are stored in PostgreSQL with the following features:
- Effective Date Management: Time-bound role assignments
- Multiple Roles: Users can have multiple roles simultaneously
- Audit Trail: Complete history of role assignments and changes
- Automatic Assignment: New users automatically receive LEARNER role
Database Schema
User Model
model User {
id String @id @default(cuid())
clerkId String @unique // Clerk user ID
email String @unique
firstName String?
lastName String?
// ... other user fields
// Role relationships
userRoles UserRole[]
}
Role Assignment Model
model UserRole {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role Role
// Effective date management
effectiveStartDate DateTime @default(now())
effectiveEndDate DateTime? // NULL means role is still active
// Assignment metadata
assignedBy String? // Clerk user ID of admin who assigned the role
assignedByEmail String? // Email of admin for display
reason String? // Reason for assignment/removal
adminNotes String? // Admin notes
// Status
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Implementation
Server-side Authentication
// lib/auth.ts
import { auth } from "@clerk/nextjs/server";
import { prisma } from "@/lib/prisma";
import { Role } from "@prisma/client";
export async function getCurrentUser() {
const { userId: clerkId } = await auth();
if (!clerkId) {
return null;
}
const user = await prisma.user.findUnique({
where: { clerkId },
include: {
userRoles: {
where: {
isActive: true,
OR: [
{ effectiveEndDate: null },
{ effectiveEndDate: { gt: new Date() } },
],
},
},
},
});
if (!user) return null;
return {
...user,
roles: user.userRoles.map((ur) => ur.role),
};
}
Role Guard Middleware
// lib/rbac.ts
export function withRoleGuard(
handler: (req: NextRequest, context?: any) => Promise<NextResponse>,
options: RoleGuardOptions,
) {
return async (req: NextRequest, context?: any) => {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const userRoles = user.roles;
const hasRequiredRole = options.requireAll
? options.roles.every(role => userRoles.includes(role))
: options.roles.some(role => userRoles.includes(role));
if (!hasRequiredRole) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
return handler(req, context);
};
}
Client-side Authentication Hook
// lib/hooks/useAuth.ts
import { useUser } from '@clerk/nextjs';
import { useQuery } from '@tanstack/react-query';
export function useAuth() {
const { user: clerkUser, isLoaded } = useUser();
const { data: user, isLoading } = useQuery({
queryKey: ['user', clerkUser?.id],
queryFn: async () => {
const response = await fetch('/api/user/profile');
return response.json();
},
enabled: isLoaded && !!clerkUser,
});
return {
user,
isLoading: isLoading || !isLoaded,
isAuthenticated: !!user,
roles: user?.roles || [],
};
}
API Protection
Protecting API Routes
// app/api/admin/users/route.ts
import { withRoleGuard } from '@/lib/rbac';
import { Role } from '@prisma/client';
export const GET = withRoleGuard(
async (req: NextRequest) => {
// Admin-only logic here
const users = await prisma.user.findMany();
return NextResponse.json(users);
},
{ roles: [Role.ADMIN] }
);
Multiple Role Requirements
// Require either MENTOR or ADMIN role
export const POST = withRoleGuard(
async (req: NextRequest) => {
// Mentor or Admin logic here
},
{ roles: [Role.MENTOR, Role.ADMIN] }
);
// Require both MENTOR and ADMIN roles
export const PUT = withRoleGuard(
async (req: NextRequest) => {
// Both roles required logic here
},
{ roles: [Role.MENTOR, Role.ADMIN], requireAll: true }
);
User Synchronization
Automatic User Sync
When a user first logs in with Clerk, the system automatically:
- Creates User Record: Adds user to PostgreSQL database
- Assigns Default Role: Automatically assigns LEARNER role
- Syncs Profile Data: Updates user information from Clerk
// app/api/auth/sync/route.ts
export async function POST(req: NextRequest) {
const { userId, email, firstName, lastName } = await req.json();
const user = await prisma.user.upsert({
where: { clerkId: userId },
update: {
email,
firstName,
lastName,
},
create: {
clerkId: userId,
email,
firstName,
lastName,
userRoles: {
create: {
role: Role.LEARNER,
effectiveStartDate: new Date(),
},
},
},
});
return NextResponse.json(user);
}
Security Features
JWT Token Security
- Tokens contain minimal user information
- Role validation performed server-side
- Automatic token refresh via Clerk
Server-side Validation
- All role checks performed on the server
- No client-side role bypassing possible
- Comprehensive audit logging
Effective Date Management
- Time-bound role assignments
- Automatic role expiration
- Historical role tracking
Migration from Keycloak
The system was migrated from Keycloak to Clerk.js for improved:
- Developer Experience: Simpler integration and configuration
- Performance: Faster authentication flows
- Maintenance: Reduced complexity and dependencies
- Flexibility: Better control over user data and roles
For migration details, see our Migration Guide.
Next Steps
- Setup Guide: Configure Clerk.js integration
- Role Management: Manage user roles and permissions
- API Reference: Complete API documentation