Authentication
Authentication is a critical security component that verifies user identity before granting access to protected resources. This document outlines the authentication mechanisms implemented in our application.
Overview
Our application uses NextAuth.js to handle authentication, providing a secure and flexible solution with multiple authentication providers. The implementation supports:
- Credential-based authentication (username/password)
- OAuth providers (GitHub)
- JWT-based sessions
- Role-based access control
- Customer context switching for B2B scenarios
Architecture
The authentication flow follows these steps:
- User initiates sign-in through a provider
- NextAuth validates credentials or processes OAuth flow
- Upon successful authentication, JWT tokens are generated
- Session data is maintained using the JWT strategy
- User roles and customer context are attached to the session
Configuration
Core Setup
The main authentication configuration is defined in auth.ts
:
export const nextAuthResult = NextAuth({
adapter: PrismaAdapter(prisma),
providers: providers,
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30 days
},
callbacks: {
jwt: async (params) => {
return mockJwtCallback(params);
},
session: async ({ session, token }) => {
if (session.user) {
session.user.role = token?.role;
session.user.id = token?.id as string;
session.user.customer = token?.customer;
session.accessToken = token.accessToken;
}
return session;
},
},
pages: {
signIn: '/login',
error: '/error',
}
});
Authentication Providers
Providers are configured in auth.providers.ts
:
export const providers: Provider[] = [
Credentials({
credentials: {
username: { label: 'Username', placeholder: 'admin', type: 'text' },
password: { label: 'Password', placeholder: 'admin', type: 'password' },
},
authorize: async (credentials) => {
// Validate and authenticate user
}
}),
GitHub({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
profile(profile) {
return {
id: profile.id.toString(),
email: profile.email,
role: 'selfservice_user',
name: profile.name ?? profile.login,
};
},
}),
];
Authentication Methods
Credential Authentication
Users can authenticate with email and password. Passwords are hashed using bcrypt for security:
authorize: async (credentials) => {
try {
const { username, password } = await signInSchema.parseAsync(credentials);
const user = await prisma.user.findUnique({
where: { email: username },
});
if (!user || !user.password) {
throw new Error('Invalid credentials');
}
const isValidPassword = await compare(password, user.password);
if (!isValidPassword) {
throw new Error('Invalid credentials');
}
return user as User;
} catch (error) {
if (error instanceof ZodError) {
throw new Error('Validation error');
} else {
throw new Error('Authentication error');
}
}
}
OAuth Authentication
The application supports GitHub OAuth authentication with custom profile mapping:
GitHub({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
profile(profile) {
return {
id: profile.id.toString(),
email: profile.email,
role: 'selfservice_user',
name: profile.name ?? profile.login,
};
},
})
Session Management
Sessions are managed using JWT tokens with a 30-day expiration by default. The JWT contains user information including:
// JWT structure from types.d.ts
interface JWT {
accessToken: string;
accessTokenExpires: number;
role?: string;
customer?: {
id: string;
roles: string[];
name: string;
};
}
The session is then populated with this data:
session: async ({ session, token }) => {
if (session.user) {
session.user.role = token?.role;
session.user.id = token?.id as string;
session.user.customer = token?.customer;
session.accessToken = token.accessToken;
}
return session;
}
User Roles and Permissions
The application implements role-based access control with predefined roles:
// From prisma schema
enum Role {
selfservice_admin
selfservice_user
}
User roles are attached to the JWT during authentication and can be used to control access to protected resources.
Customer Context
For B2B scenarios, users can be associated with customer accounts and switch between them:
async function updateCustomerToken(token: JWT, customerId: string | undefined) {
try {
const accessToken = signUserToken(token);
const customer = customerId
? await sdk.users.getCustomerForCurrentUserById({ id: customerId }, accessToken)
: await sdk.users.getDefaultCustomerForCurrentUser(accessToken);
if (customer) {
token.customer = {
id: customer.id,
roles: customer?.roles?.map((role) => role.role) ?? [],
name: customer?.name ?? '',
};
}
} catch (error) {
throw new Error('Error fetching customer data');
}
}
This allows users to:
- Have a default customer context
- Switch between multiple customer accounts they have access to
- Have different roles for different customers
API Routes
NextAuth.js API routes are configured in app/api/auth/[...nextauth]/route.ts
:
import { handlers } from '@/auth';
export const { GET, POST } = handlers;
Security Best Practices
Password Storage
Passwords are hashed using bcrypt before storage:
// From seed.ts
{
id: 'admin-1',
name: 'Jane Doe',
email: 'jane@example.com',
password: await hash('admin', 10), // Hashed with bcrypt
role: Role.selfservice_admin,
defaultCustomerId: 'cust-001',
}
JWT Signing
In production, JWTs should be signed with a secure secret:
function signUserToken(token: JWT): string {
return jwt.sign(
{
name: token.name,
email: token.email,
role: token.role,
customer: token?.customer
? {
id: token.customer.id,
roles: token.customer.roles,
name: token.customer.name,
}
: undefined,
},
process.env.JWT_SECRET || 'secret',
);
}
Input Validation
User inputs are validated using Zod schemas:
export const signInSchema = object({
username: string().email('Must be a valid email'),
password: string().min(4, 'Password must be at least 4 characters'),
});
Implementation Guidelines
Securing Routes
Use the auth()
function to protect routes:
import { auth } from '@/auth';
export default async function ProtectedPage() {
const session = await auth();
if (!session) {
redirect('/login');
}
// Render protected content
}
Role-Based Access Control
Check user roles to control access to features:
if (session?.user?.role === 'selfservice_admin') {
// Show admin features
}
Customer Context Switching
To implement customer switching:
// Update session with new customer context
await update({
customerId: selectedCustomerId,
});
Extending Authentication
Adding New Providers
To add a new authentication provider:
- Install the required package
- Add provider configuration to
auth.providers.ts
- Update UI to include the new sign-in option
Custom User Data
To store additional user data:
- Extend the Prisma User model
- Update the JWT and Session type definitions
- Modify the JWT callback to include the additional data