Skip to main content

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:

  1. User initiates authentication (registration or login)
  2. For students, Microsoft Authentication is used to verify their academic status
  3. Upon successful authentication, a JWT token is issued
  4. 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​

EndpointMethodDescriptionAuthentication Required
/auth/registerPOSTRegister a new userNo
/auth/loginPOSTAuthenticate a userNo
/auth/microsoft/initiatePOSTInitiate Microsoft AuthenticationNo
/auth/microsoft/callbackGETHandle Microsoft Authentication callbackNo
/auth/refreshPOSTRefresh authentication tokenYes
/auth/logoutPOSTLog out a userYes
/auth/password/reset-requestPOSTRequest password resetNo
/auth/password/resetPOSTReset password with tokenNo
/auth/password/changePUTChange user passwordYes
/auth/verify-emailGETVerify user emailNo

User Management​

EndpointMethodDescriptionAuthentication Required
/auth/user/profileGETGet user profileYes
/auth/user/profilePUTUpdate user profileYes
/auth/user/deleteDELETEDelete user accountYes

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