Authentication Service
The Authentication Service handles user authentication, authorization, and account management for the RovesUp platform. It provides secure authentication mechanisms, role-based access control, and integration with Microsoft Authentication for student verification.
Features
- User registration and login
- Password management with secure hashing
- Role-based access control (Student, Company, Admin)
- JWT token authentication
- Microsoft Authentication integration for student verification
- Account management (profile updates, password resets)
Technical Implementation
Authentication Flow
The authentication flow in RovesUp follows these steps:
- User initiates authentication (registration or login)
- For students, Microsoft Authentication is used to verify their academic status
- Upon successful authentication, a JWT token is issued
- The JWT token is used for subsequent API requests
Microsoft Authentication Integration
We use Microsoft Authentication to verify that users registering as students have valid academic email addresses. This ensures that only actual students can access student-specific features.
// Microsoft Authentication configuration
import { ConfidentialClientApplication } from '@azure/msal-node';
const msalConfig = {
auth: {
clientId: process.env.MICROSOFT_CLIENT_ID,
authority: `https://login.microsoftonline.com/${process.env.MICROSOFT_TENANT_ID}`,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
}
};
const msalClient = new ConfidentialClientApplication(msalConfig);
// Function to initiate Microsoft Authentication
export async function initiateStudentVerification(req: Request, res: Response): Promise<void> {
try {
const authCodeUrlParameters = {
scopes: ['user.read', 'mail.read'],
redirectUri: process.env.MICROSOFT_REDIRECT_URI,
state: generateStateParameter(req.body.email),
};
const response = await msalClient.getAuthCodeUrl(authCodeUrlParameters);
res.json({ authUrl: response });
} catch (error) {
logger.error('Error initiating Microsoft authentication', error);
res.status(500).json({ error: 'Failed to initiate authentication' });
}
}
// Function to handle Microsoft Authentication callback
export async function handleMicrosoftCallback(req: Request, res: Response): Promise<void> {
try {
const tokenRequest = {
code: req.query.code as string,
scopes: ['user.read', 'mail.read'],
redirectUri: process.env.MICROSOFT_REDIRECT_URI,
};
const response = await msalClient.acquireTokenByCode(tokenRequest);
// Verify the email domain is from an academic institution
const userEmail = response.account.username;
if (!isAcademicEmail(userEmail)) {
return res.status(403).json({ error: 'Email domain not recognized as an academic institution' });
}
// Create or update user with verified student status
const user = await createOrUpdateVerifiedStudent(userEmail, response.account);
// Generate JWT token
const token = generateJwtToken(user);
res.json({ token, user: sanitizeUser(user) });
} catch (error) {
logger.error('Error handling Microsoft callback', error);
res.status(500).json({ error: 'Authentication failed' });
}
}
// Helper function to check if email is from an academic institution
function isAcademicEmail(email: string): boolean {
const academicDomains = [
'.edu', '.ac.uk', '.edu.fr', '.ac.fr', '.edu.au',
// List of verified French academic institutions
'univ-paris1.fr', 'sorbonne-universite.fr', 'u-bordeaux.fr',
'univ-lyon1.fr', 'univ-grenoble-alpes.fr', 'univ-lille.fr',
'polytechnique.edu', 'centralesupelec.fr', 'hec.edu',
'essec.edu', 'edhec.edu', 'em-lyon.com'
];
return academicDomains.some(domain => email.toLowerCase().endsWith(domain));
}
Password Hashing
For accounts that use password-based authentication (primarily company accounts), we implement secure password hashing using bcrypt with appropriate salt rounds:
import * as bcrypt from 'bcrypt';
// Constants
const SALT_ROUNDS = 12;
// Hash a password
export async function hashPassword(password: string): Promise<string> {
try {
const salt = await bcrypt.genSalt(SALT_ROUNDS);
return bcrypt.hash(password, salt);
} catch (error) {
logger.error('Error hashing password', error);
throw new Error('Password hashing failed');
}
}
// Verify a password
export async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
try {
return await bcrypt.compare(password, hashedPassword);
} catch (error) {
logger.error('Error verifying password', error);
throw new Error('Password verification failed');
}
}
// Example usage in user registration
export async function registerUser(req: Request, res: Response): Promise<void> {
try {
const { email, password, userType } = req.body;
// Validate input
if (!email || !password || !userType) {
return res.status(400).json({ error: 'Missing required fields' });
}
// Check if user already exists
const existingUser = await UserModel.findOne({ email });
if (existingUser) {
return res.status(409).json({ error: 'User already exists' });
}
// Hash password
const hashedPassword = await hashPassword(password);
// Create new user
const user = new UserModel({
email,
password: hashedPassword,
userType,
createdAt: new Date(),
updatedAt: new Date()
});
await user.save();
// Generate JWT token
const token = generateJwtToken(user);
res.status(201).json({ token, user: sanitizeUser(user) });
} catch (error) {
logger.error('Error registering user', error);
res.status(500).json({ error: 'Registration failed' });
}
}
JWT Token Authentication
We use JSON Web Tokens (JWT) for stateless authentication. Tokens are signed with a secure algorithm and include user information and permissions:
import * as jwt from 'jsonwebtoken';
// JWT configuration
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRY = '24h';
// Generate a JWT token
export function generateJwtToken(user: IUser): string {
const payload = {
id: user._id,
email: user.email,
userType: user.userType,
permissions: getUserPermissions(user),
};
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRY });
}
// Verify a JWT token
export function verifyJwtToken(token: string): any {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
logger.error('Error verifying JWT token', error);
throw new Error('Invalid token');
}
}
// Authentication middleware
export function authenticate(req: Request, res: Response, next: NextFunction): void {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authentication required' });
}
const token = authHeader.split(' ')[1];
const decoded = verifyJwtToken(token);
// Attach user to request
req.user = decoded;
next();
} catch (error) {
logger.error('Authentication failed', error);
res.status(401).json({ error: 'Authentication failed' });
}
}
// Authorization middleware
export function authorize(roles: string[]): (req: Request, res: Response, next: NextFunction) => void {
return (req: Request, res: Response, next: NextFunction): void => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.userType)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
Database Schema
The Authentication Service uses the following MongoDB schema for user management:
import mongoose, { Schema, Document } from 'mongoose';
export interface IUser extends Document {
email: string;
password?: string;
userType: 'student' | 'company' | 'admin';
microsoftId?: string;
profile: {
firstName?: string;
lastName?: string;
phoneNumber?: string;
profilePicture?: string;
};
isEmailVerified: boolean;
isStudentVerified: boolean;
academicInstitution?: string;
lastLogin?: Date;
createdAt: Date;
updatedAt: Date;
}
const UserSchema: Schema = new Schema({
email: { type: String, required: true, unique: true },
password: { type: String },
userType: { type: String, enum: ['student', 'company', 'admin'], required: true },
microsoftId: { type: String },
profile: {
firstName: { type: String },
lastName: { type: String },
phoneNumber: { type: String },
profilePicture: { type: String }
},
isEmailVerified: { type: Boolean, default: false },
isStudentVerified: { type: Boolean, default: false },
academicInstitution: { type: String },
lastLogin: { type: Date },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
// Pre-save hook to update the updatedAt field
UserSchema.pre('save', function(next) {
this.updatedAt = new Date();
next();
});
export const UserModel = mongoose.model<IUser>('User', UserSchema);
API Endpoints
Authentication
| Endpoint | Method | Description | Authentication Required |
|---|---|---|---|
/auth/register | POST | Register a new user | No |
/auth/login | POST | Authenticate a user | No |
/auth/microsoft/initiate | POST | Initiate Microsoft Authentication | No |
/auth/microsoft/callback | GET | Handle Microsoft Authentication callback | No |
/auth/refresh | POST | Refresh authentication token | Yes |
/auth/logout | POST | Log out a user | Yes |
/auth/password/reset-request | POST | Request password reset | No |
/auth/password/reset | POST | Reset password with token | No |
/auth/password/change | PUT | Change user password | Yes |
/auth/verify-email | GET | Verify user email | No |
User Management
| Endpoint | Method | Description | Authentication Required |
|---|---|---|---|
/auth/user/profile | GET | Get user profile | Yes |
/auth/user/profile | PUT | Update user profile | Yes |
/auth/user/delete | DELETE | Delete user account | Yes |
Security Considerations
- Passwords are hashed using bcrypt with a salt factor of 12
- JWT tokens are signed with a secure algorithm (HS256)
- Rate limiting is implemented to prevent brute force attacks
- HTTPS is required for all API calls
- Microsoft Authentication is used to verify student status
- Input validation is performed on all endpoints
- Sensitive data is never logged or exposed in responses
Error Handling
The Authentication Service uses consistent error handling:
// Error response structure
interface ErrorResponse {
error: string;
details?: any;
code?: string;
}
// Common error handling middleware
export function errorHandler(err: any, req: Request, res: Response, next: NextFunction): void {
logger.error('Error occurred', err);
// Handle specific error types
if (err.name === 'ValidationError') {
return res.status(400).json({
error: 'Validation error',
details: err.errors
});
}
if (err.name === 'UnauthorizedError') {
return res.status(401).json({
error: 'Authentication error',
code: 'UNAUTHORIZED'
});
}
// Default error response
res.status(500).json({
error: 'Internal server error',
code: 'INTERNAL_ERROR'
});
}
Integration with Other Services
The Authentication Service integrates with other RovesUp services:
- Student Service: Provides student verification status
- Company Service: Provides company verification status
- Notification Service: Triggers notifications for authentication events (e.g., password reset)
Deployment
The Authentication Service is deployed as a Docker container in a Kubernetes cluster:
# Kubernetes deployment configuration
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-service
namespace: rovesup
spec:
replicas: 3
selector:
matchLabels:
app: auth-service
template:
metadata:
labels:
app: auth-service
spec:
containers:
- name: auth-service
image: registry.gitlab.com/rovesup/auth-service:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: MONGODB_URI
valueFrom:
secretKeyRef:
name: auth-secrets
key: mongodb-uri
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: auth-secrets
key: jwt-secret
- name: MICROSOFT_CLIENT_ID
valueFrom:
secretKeyRef:
name: auth-secrets
key: microsoft-client-id
- name: MICROSOFT_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: auth-secrets
key: microsoft-client-secret
- name: MICROSOFT_TENANT_ID
valueFrom:
secretKeyRef:
name: auth-secrets
key: microsoft-tenant-id
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "200m"
memory: "256Mi"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10