@bernierllc/auth-service
v1.0.5
Published
Complete authentication and authorization service with multi-provider support and session management
Readme
@bernierllc/auth-service
Complete authentication and authorization service with multi-provider support and session management.
Features
- 🔐 Multi-Provider Authentication - OAuth2, SAML, LDAP, local authentication
- 🎫 JWT Token Management - Secure JWT generation and verification using crypto-utils
- 🔄 Refresh Tokens - Automatic token renewal
- 🛡️ Security Features - Rate limiting, account lockout, password policies
- 📧 Password Reset - Magic link based password reset using crypto-utils
- 👥 User Management - Registration, profile management
- 📊 Audit Logging - Comprehensive authentication event logging
- 🔒 Role-Based Access - RBAC with permissions (coming soon)
Installation
npm install @bernierllc/auth-serviceQuick Start
import { AuthService } from '@bernierllc/auth-service';
// Initialize auth service
const authService = new AuthService({
jwt: {
secret: process.env.JWT_SECRET || 'your-secret-key',
expiresIn: '15m',
refreshExpiresIn: '7d'
},
password: {
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSymbols: true
},
security: {
maxLoginAttempts: 5,
lockoutDuration: 15 * 60 * 1000, // 15 minutes
requireEmailVerification: false
}
});
// Register a new user
const registerResult = await authService.register({
username: 'johndoe',
email: '[email protected]',
password: 'SecurePass123!',
firstName: 'John',
lastName: 'Doe',
acceptTerms: true
});
if (registerResult.success) {
console.log('User registered:', registerResult.user);
console.log('Access token:', registerResult.token);
}
// Login user
const loginResult = await authService.login({
email: '[email protected]',
password: 'SecurePass123!'
});
if (loginResult.success) {
console.log('User logged in:', loginResult.user);
console.log('Access token:', loginResult.token);
console.log('Refresh token:', loginResult.refreshToken);
}
// Verify token
const user = await authService.verifyToken(loginResult.token!);
if (user) {
console.log('Token valid for user:', user.email);
}
// Refresh token
const refreshResult = await authService.refreshToken(loginResult.refreshToken!);
if (refreshResult.success) {
console.log('New access token:', refreshResult.token);
}Password Reset Flow
// Generate password reset link
const resetResult = await authService.generatePasswordResetLink('[email protected]');
if (resetResult.success) {
console.log('Password reset link sent');
// In production, this would send an email with the reset link
}
// Reset password using token from email link
const newPassword = 'NewSecurePass123!';
const resetPasswordResult = await authService.resetPassword(resetToken, newPassword);
if (resetPasswordResult.success) {
console.log('Password reset successfully');
}Configuration
JWT Configuration
{
jwt: {
secret: string; // JWT signing secret
issuer?: string; // JWT issuer claim
audience?: string; // JWT audience claim
expiresIn?: string | number; // Access token expiry (default: '15m')
refreshExpiresIn?: string | number; // Refresh token expiry (default: '7d')
algorithm?: 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512';
}
}Password Policy
{
password: {
minLength: number; // Minimum password length
requireUppercase: boolean; // Require uppercase letters
requireLowercase: boolean; // Require lowercase letters
requireNumbers: boolean; // Require numbers
requireSymbols: boolean; // Require special characters
preventReuse: number; // Prevent reusing last N passwords
maxAge?: number; // Password expiry in milliseconds
}
}Security Settings
{
security: {
maxLoginAttempts: number; // Max failed attempts before lockout
lockoutDuration: number; // Account lockout duration (ms)
sessionTimeout: number; // Session timeout (ms)
requireEmailVerification: boolean; // Require email verification
allowMultipleSessions: boolean; // Allow multiple concurrent sessions
}
}MFA Configuration (Coming Soon)
{
mfa: {
enabled: boolean;
issuer?: string;
window?: number;
backupCodes: {
enabled: boolean;
count: number;
length: number;
};
}
}API Reference
Class: AuthService
register(credentials: RegisterCredentials): Promise<AuthResult>
Register a new user account.
Parameters:
credentials.username- Unique usernamecredentials.email- Email addresscredentials.password- Password (must meet policy requirements)credentials.firstName- Optional first namecredentials.lastName- Optional last namecredentials.acceptTerms- Must be truecredentials.emailMarketing- Optional marketing consent
Returns: Promise resolving to AuthResult with user, token, and refreshToken
login(credentials: LoginCredentials): Promise<AuthResult>
Authenticate user login.
Parameters:
credentials.emailorcredentials.username- User identifiercredentials.password- Passwordcredentials.rememberMe- Optional remember me flag
Returns: Promise resolving to AuthResult with user, token, and refreshToken
verifyToken(token: string): Promise<User | null>
Verify and decode JWT access token.
Returns: Promise resolving to User object or null if invalid
refreshToken(refreshToken: string): Promise<AuthResult>
Generate new access token using refresh token.
Returns: Promise resolving to AuthResult with new tokens
generatePasswordResetLink(email: string): Promise<{success: boolean; error?: string}>
Generate password reset link for user.
resetPassword(token: string, newPassword: string): Promise<AuthResult>
Reset user password using magic link token.
Third-Party Authentication Adapters
The auth-service supports integration with third-party authentication providers through a flexible adapter pattern. This allows you to:
- Use external authentication services (Clerk, Auth0, Firebase, etc.) for user authentication
- Maintain internal authorization and permission management
- Seamlessly integrate multiple authentication methods
Supported Adapters
- ✅ ClerkAdapter - Clerk authentication service integration
- 🔄 Auth0Adapter - Auth0 platform integration (coming soon)
- 🔄 FirebaseAdapter - Firebase Authentication (coming soon)
- 🔄 AzureADAdapter - Microsoft Azure Active Directory (coming soon)
🚀 Quick Start: Adding to Existing Authentication
Already have Clerk, Auth0, or another auth provider? You can add auth-service for authorization and permission management without changing your existing authentication flow.
Scenario: You Already Have Clerk Authentication
If you already have Clerk set up in your application, you can add auth-service to handle permissions and role-based access control:
Step 1: Install and Configure
// your-auth.ts - Add alongside your existing Clerk setup
import { AuthService, ClerkAdapter } from '@bernierllc/auth-service';
export const authService = new AuthService({
jwt: {
secret: process.env.JWT_SECRET!, // Different from Clerk's secret
expiresIn: '1h'
},
// Configure the Clerk adapter
adapters: [
new ClerkAdapter({
secretKey: process.env.CLERK_SECRET_KEY!, // Same as your existing Clerk setup
publishableKey: process.env.CLERK_PUBLISHABLE_KEY!, // Same as your existing Clerk setup
jwtKey: process.env.CLERK_JWT_KEY, // Optional - for better performance
// Define your application's permissions
roleMapping: {
// Map Clerk roles/orgs to your app's permissions
'admin': ['*'], // Full access
'content-manager': [
'content:read', 'content:write', 'content:delete',
'workflow:approve', 'analytics:view'
],
'editor': [
'content:read', 'content:write', 'workflow:approve'
],
'author': [
'content:read', 'content:write'
],
'viewer': [
'content:read'
]
}
})
],
// Disable local authentication - using Clerk for that
providers: [],
security: {
maxLoginAttempts: 10, // Clerk handles rate limiting
lockoutDuration: 30 * 60 * 1000,
requireEmailVerification: false // Clerk handles this
}
});Step 2: Update Your Middleware (Keep Existing Clerk Code)
// middleware.ts - Your existing Clerk middleware with added permissions
import { auth } from '@clerk/nextjs/server';
import { authService } from './your-auth';
// Your existing Clerk middleware (keep as-is)
export default auth((req) => {
// Your existing Clerk logic...
});
// Add permission checking helpers
export async function checkPermission(permission: string) {
const { userId, sessionClaims } = auth();
if (!userId) return false;
// Use auth-service for permission checking
return await authService.hasPermission(userId, permission);
}
export async function requirePermission(permission: string) {
const hasPermission = await checkPermission(permission);
if (!hasPermission) {
throw new Error(`Permission denied: ${permission}`);
}
}Step 3: Add Permission Checks to Your Routes
// app/api/content/route.ts - Example API route
import { auth } from '@clerk/nextjs/server';
import { authService } from '@/lib/your-auth';
import { NextResponse } from 'next/server';
export async function GET() {
const { userId, getToken } = auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Check permission using auth-service
const canRead = await authService.hasPermission(userId, 'content:read');
if (!canRead) {
return NextResponse.json({ error: 'Permission denied' }, { status: 403 });
}
// Your existing content logic...
return NextResponse.json({ content: 'Your content here' });
}
export async function POST(request: Request) {
const { userId } = auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Check write permission
const canWrite = await authService.hasPermission(userId, 'content:write');
if (!canWrite) {
return NextResponse.json({ error: 'Permission denied' }, { status: 403 });
}
// Your content creation logic...
return NextResponse.json({ success: true });
}Step 4: Frontend Permission Checks (React/Next.js)
// hooks/usePermissions.ts - Custom hook for frontend permission checking
import { useAuth, useUser } from '@clerk/nextjs';
import { useState, useEffect } from 'react';
export function usePermission(permission: string) {
const { userId, getToken } = useAuth();
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
useEffect(() => {
async function checkPermission() {
if (!userId) {
setHasPermission(false);
return;
}
try {
const token = await getToken();
// Call your API to check permission
const response = await fetch('/api/auth/check-permission', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ permission })
});
const result = await response.json();
setHasPermission(result.hasPermission);
} catch (error) {
console.error('Permission check failed:', error);
setHasPermission(false);
}
}
checkPermission();
}, [userId, permission]);
return hasPermission;
}
// API route: app/api/auth/check-permission/route.ts
import { auth } from '@clerk/nextjs/server';
import { authService } from '@/lib/your-auth';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { userId } = auth();
if (!userId) {
return NextResponse.json({ hasPermission: false });
}
const { permission } = await request.json();
const hasPermission = await authService.hasPermission(userId, permission);
return NextResponse.json({ hasPermission });
}Step 5: Component-Level Permission Control
// components/ProtectedContent.tsx - Conditional rendering based on permissions
import { usePermission } from '@/hooks/usePermissions';
import { useUser } from '@clerk/nextjs';
interface ProtectedContentProps {
permission: string;
children: React.ReactNode;
fallback?: React.ReactNode;
}
export function ProtectedContent({ permission, children, fallback }: ProtectedContentProps) {
const { isLoaded, isSignedIn } = useUser();
const hasPermission = usePermission(permission);
if (!isLoaded) {
return <div>Loading...</div>;
}
if (!isSignedIn) {
return fallback || <div>Please sign in</div>;
}
if (hasPermission === null) {
return <div>Checking permissions...</div>;
}
if (!hasPermission) {
return fallback || <div>Access denied</div>;
}
return <>{children}</>;
}
// Usage in your components
export function ContentManager() {
return (
<div>
<h1>Content Manager</h1>
{/* Show create button only if user can write */}
<ProtectedContent permission="content:write">
<button>Create New Content</button>
</ProtectedContent>
{/* Show approve button only if user can approve */}
<ProtectedContent permission="workflow:approve">
<button>Approve Content</button>
</ProtectedContent>
{/* Show analytics only for managers */}
<ProtectedContent permission="analytics:view">
<AnalyticsPanel />
</ProtectedContent>
</div>
);
}Alternative: Token-Based Integration (Any Auth Provider)
If you're using a different auth provider or want a more generic approach:
// auth-integration.ts - Generic token-based integration
import { AuthService } from '@bernierllc/auth-service';
const authService = new AuthService({
jwt: { secret: process.env.JWT_SECRET! },
// No adapters needed for generic integration
adapters: [],
providers: [],
security: {
maxLoginAttempts: 5,
lockoutDuration: 15 * 60 * 1000,
requireEmailVerification: false
}
});
// Custom permission checker that works with any auth provider
export async function checkUserPermission(userId: string, permission: string): Promise<boolean> {
// You can implement custom logic here:
// Option 1: Check against a database
const user = await getUserFromDatabase(userId);
return user.permissions.includes(permission) || user.roles.some(role =>
getRolePermissions(role).includes(permission)
);
// Option 2: Use auth-service's built-in permission system
return await authService.hasPermission(userId, permission);
}
// Middleware for any framework
export function requirePermissionMiddleware(permission: string) {
return async (req: any, res: any, next: any) => {
const userId = req.user?.id; // However you get user ID from your auth provider
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const hasPermission = await checkUserPermission(userId, permission);
if (!hasPermission) {
return res.status(403).json({ error: `Permission denied: ${permission}` });
}
next();
};
}Key Benefits of This Approach
- 🔄 No Migration Required - Keep your existing authentication flow
- 🛡️ Enhanced Security - Add fine-grained permissions without touching auth
- 📈 Scalable - Easily add new permissions as your app grows
- 🔗 Framework Agnostic - Works with Next.js, Express, Fastify, etc.
- ⚡ Performance - Efficient permission checking with caching support
- 🧪 Testable - Easy to test permission logic independently
Migration Timeline
Week 1: Install and configure auth-service
Week 2: Add permission checks to critical routes
Week 3: Implement frontend permission components
Week 4: Roll out to all features
This approach lets you gradually add authorization features without disrupting your existing authentication setup.
Basic Adapter Usage
import { AuthService, ClerkAdapter } from '@bernierllc/auth-service';
const authService = new AuthService({
// Standard configuration...
jwt: {
secret: process.env.JWT_SECRET!,
expiresIn: '15m'
},
// Add third-party adapters
adapters: [
new ClerkAdapter({
secretKey: process.env.CLERK_SECRET_KEY!,
publishableKey: process.env.CLERK_PUBLISHABLE_KEY!,
jwtKey: process.env.CLERK_JWT_KEY, // Optional - for networkless verification
roleMapping: {
'admin': ['*'], // Admin role gets all permissions
'editor': ['content:read', 'content:write', 'workflow:approve'],
'viewer': ['content:read']
}
})
],
// RBAC configuration for internal authorization
security: {
maxLoginAttempts: 5,
lockoutDuration: 15 * 60 * 1000,
requireEmailVerification: false
}
});
// Verify third-party tokens seamlessly
const user = await authService.verifyToken(clerkJwtToken);
if (user) {
// Check permissions managed internally
const canApprove = await authService.hasPermission(user.id, 'workflow:approve');
console.log('User can approve workflows:', canApprove);
}Clerk Integration Example
import { AuthService, ClerkAdapter } from '@bernierllc/auth-service';
// Configure Clerk adapter with role mapping
const clerkAdapter = new ClerkAdapter({
secretKey: process.env.CLERK_SECRET_KEY!,
publishableKey: process.env.CLERK_PUBLISHABLE_KEY!,
jwtKey: process.env.CLERK_JWT_KEY, // For networkless token verification
domain: 'your-domain.com', // Optional custom domain
// Map Clerk roles to internal permissions
roleMapping: {
'admin': ['*'], // Admin gets all permissions
'content-manager': ['content:read', 'content:write', 'content:delete'],
'editor': ['content:read', 'content:write', 'workflow:approve'],
'author': ['content:read', 'content:write'],
'viewer': ['content:read']
}
});
const authService = new AuthService({
jwt: {
secret: process.env.JWT_SECRET!,
expiresIn: '1h'
},
adapters: [clerkAdapter],
// No local providers needed - using Clerk for authentication
providers: [],
security: {
maxLoginAttempts: 10, // Clerk handles this, but kept for local fallback
lockoutDuration: 30 * 60 * 1000,
requireEmailVerification: false // Clerk handles email verification
}
});
// Usage in your application
export class ContentController {
constructor(private authService: AuthService) {}
async approveContent(req: Request, res: Response) {
// Extract JWT from Authorization header
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
// Verify token (will use Clerk adapter automatically)
const user = await this.authService.verifyToken(token);
if (!user) {
return res.status(401).json({ error: 'Invalid token' });
}
// Check permission (managed internally by auth-service)
const canApprove = await this.authService.hasPermission(user.id, 'workflow:approve');
if (!canApprove) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
// Proceed with content approval
// ... your business logic here
res.json({ success: true, message: 'Content approved' });
}
}Express Middleware Integration
import express from 'express';
import { AuthService, ClerkAdapter } from '@bernierllc/auth-service';
const app = express();
// Initialize auth service with Clerk
const authService = new AuthService({
jwt: { secret: process.env.JWT_SECRET! },
adapters: [
new ClerkAdapter({
secretKey: process.env.CLERK_SECRET_KEY!,
publishableKey: process.env.CLERK_PUBLISHABLE_KEY!,
roleMapping: {
'admin': ['*'],
'editor': ['content:read', 'content:write', 'workflow:approve'],
'viewer': ['content:read']
}
})
]
});
// Authentication middleware
const authenticate = async (req: any, res: any, next: any) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
const user = await authService.verifyToken(token);
if (!user) {
return res.status(401).json({ error: 'Invalid token' });
}
req.user = user; // Attach user to request
next();
} catch (error) {
res.status(500).json({ error: 'Authentication error' });
}
};
// Permission middleware
const requirePermission = (permission: string) => {
return async (req: any, res: any, next: any) => {
try {
if (!req.user) {
return res.status(401).json({ error: 'No authenticated user' });
}
const hasPermission = await authService.hasPermission(req.user.id, permission);
if (!hasPermission) {
return res.status(403).json({ error: `Permission denied: ${permission}` });
}
next();
} catch (error) {
res.status(500).json({ error: 'Permission check failed' });
}
};
};
// Protected routes
app.get('/api/content', authenticate, requirePermission('content:read'), (req, res) => {
res.json({ content: 'Your content here' });
});
app.post('/api/content', authenticate, requirePermission('content:write'), (req, res) => {
// Create content logic
res.json({ success: true });
});
app.post('/api/content/:id/approve', authenticate, requirePermission('workflow:approve'), (req, res) => {
// Approve content logic
res.json({ success: true, message: 'Content approved' });
});Creating Custom Adapters
You can create custom adapters for any authentication provider by implementing the AuthAdapter interface:
import { AuthAdapter, User, BaseAuthAdapter } from '@bernierllc/auth-service';
export class CustomAuthAdapter extends BaseAuthAdapter {
name = 'custom-provider';
type = 'custom' as const;
constructor(private config: CustomAdapterConfig) {
super();
this.validateConfig(config);
}
async verifyToken(token: string): Promise<User | null> {
try {
// 1. Verify token with your auth provider
const tokenData = await this.provider.verifyJWT(token);
// 2. Get user information from token or API call
const userData = await this.provider.getUser(tokenData.userId);
// 3. Map to auth-service User format
return {
id: userData.id,
username: userData.username,
email: userData.email,
emailVerified: userData.emailVerified,
roles: this.mapRoles(userData.roles, this.config.roleMapping),
permissions: this.mapPermissions(userData.roles, this.config.roleMapping),
profile: userData.profile,
preferences: userData.preferences || {},
security: userData.security || {},
createdAt: new Date(userData.createdAt),
updatedAt: new Date(userData.updatedAt),
lastLoginAt: userData.lastLoginAt ? new Date(userData.lastLoginAt) : undefined
};
} catch (error) {
console.error('Token verification failed:', error);
return null;
}
}
async getUserById(userId: string): Promise<User | null> {
// Implement user lookup by ID
return null;
}
}
// Use your custom adapter
const authService = new AuthService({
adapters: [
new CustomAuthAdapter({
apiKey: process.env.CUSTOM_AUTH_API_KEY!,
roleMapping: {
'superuser': ['*'],
'contributor': ['content:read', 'content:write']
}
})
]
});Best Practices
- Security: Always validate tokens server-side
- Role Mapping: Define clear role-to-permission mappings
- Error Handling: Handle adapter failures gracefully
- Logging: Monitor authentication and authorization events
- Testing: Test with real tokens from your auth provider
Integration with Crypto-Utils
This service leverages @bernierllc/crypto-utils for:
- JWT Generation & Verification - Secure token handling
- Magic Links - Password reset functionality
- API Key Generation - Secure random key generation
- Password Hashing - Using bcrypt for secure password storage
Dependencies
@bernierllc/crypto-utils- Cryptographic utilities@bernierllc/logger- Structured loggingbcrypt- Password hashinguuid- Unique ID generation
Integration Status
- Logger: integrated - Uses @bernierllc/logger for authentication event logging and security monitoring
- NeverHub: not-applicable - Auth service operates independently without service discovery requirements
- Docs-Suite: ready - Complete documentation with TypeScript examples, API reference, and integration guides
License
Copyright (c) 2025 Bernier LLC. All rights reserved.
