Skip to main content

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

  1. Clerk.js: Handles user authentication, session management, and JWT tokens
  2. PostgreSQL: Stores user data and role assignments with effective date management
  3. Custom JWT System: Embeds role information in tokens for efficient authorization
  4. 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:

  1. Creates User Record: Adds user to PostgreSQL database
  2. Assigns Default Role: Automatically assigns LEARNER role
  3. 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