ideas-generator/server/middleware/azureAuth.js
DJP 013f57fe60 Implement hybrid Azure AD SSO + Password authentication system
 Backend Implementation:
- Add Azure AD JWT token validation middleware
- Create hybrid authentication system supporting both Azure AD and password auth
- Implement auto-provisioning for new Azure AD users
- Add admin controls to toggle password authentication
- Update all API routes to use hybrid authentication
- Add database fields for authentication (password, lastLoginAt)
- Create comprehensive auth routes with validation endpoints

 Frontend Implementation:
- Install and configure Azure MSAL browser library
- Create Azure AD authentication service with popup/redirect support
- Build hybrid authentication service managing both auth methods
- Update Login.vue with modern dual-authentication UI
- Implement dynamic password auth toggle based on admin settings
- Update App.vue for proper session management and validation
- Modify API service to handle both token types

 Security Features:
- Azure AD tenant validation (Oliver Agency)
- Role-based access control with auto-admin assignment
- JWT token validation for both auth methods
- Automatic user provisioning with proper defaults
- Session validation and automatic logout on token expiry

 Admin Features:
- Toggle password authentication on/off
- Manage users from both authentication methods
- Full role and agent access control
- Azure AD user auto-provisioning as regular users

 Configuration:
- Azure AD: Tenant e519c2e6-bc6d-4fdf-8d9c-923c2f002385
- Client ID: 9079054c-9620-4757-a256-23413042f1ef
- Development redirect URI support
- Fallback password authentication for testing

🔧 Technical Stack:
- Azure MSAL Browser & Node libraries
- JWT token validation and hybrid middleware
- Database schema updates with migrations
- Vue.js integration with MSAL
- Express.js hybrid authentication routes

🚀 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-09 16:14:02 -04:00

134 lines
No EOL
4.2 KiB
JavaScript

const jwt = require('jsonwebtoken');
const { jwtDecode } = require('jwt-decode');
const User = require('../models/User');
// Azure AD configuration
const AZURE_TENANT_ID = 'e519c2e6-bc6d-4fdf-8d9c-923c2f002385';
const AZURE_CLIENT_ID = '9079054c-9620-4757-a256-23413042f1ef';
const validateAzureToken = async (req, res, next) => {
try {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ message: 'Access token required' });
}
// Decode the Azure AD token (without verification for now - in production should verify signature)
let decoded;
try {
decoded = jwtDecode(token);
} catch (error) {
return res.status(403).json({ message: 'Invalid token format' });
}
// Validate Azure AD token claims
if (decoded.aud !== AZURE_CLIENT_ID) {
return res.status(403).json({ message: 'Invalid token audience' });
}
if (decoded.tid !== AZURE_TENANT_ID) {
return res.status(403).json({ message: 'Invalid tenant' });
}
if (decoded.exp * 1000 < Date.now()) {
return res.status(403).json({ message: 'Token expired' });
}
// Check if user exists in database, create if not
let user = await User.findOne({ where: { email: decoded.email || decoded.upn } });
if (!user) {
// Auto-provision new Azure AD user
user = await User.create({
email: decoded.email || decoded.upn,
name: decoded.name || decoded.given_name + ' ' + decoded.family_name,
password: 'azure-ad-user', // Placeholder - not used for Azure users
preferences: {
theme: 'light',
notifications: true,
defaultAssistant: 'creator-bot-push-the-boundaries-of-technology',
role: getAzureUserRole(decoded.email || decoded.upn),
allowedAgents: getAzureUserRole(decoded.email || decoded.upn) === 'admin' ? null : [],
authMethod: 'azure'
},
isActive: true
});
} else {
// Update last login for existing user
await user.update({
lastLoginAt: new Date(),
preferences: {
...user.preferences,
authMethod: 'azure'
}
});
}
if (!user.isActive) {
return res.status(403).json({ message: 'User account is disabled' });
}
// Attach user info to request
req.user = {
id: user.id,
email: user.email,
name: user.name,
role: user.preferences?.role || 'user',
allowedAgents: user.preferences?.allowedAgents || null,
authMethod: 'azure',
azureId: decoded.oid || decoded.sub
};
next();
} catch (error) {
console.error('Azure token validation error:', error);
return res.status(500).json({ message: 'Token validation failed' });
}
};
// Determine user role based on email or Azure group membership
const getAzureUserRole = (email) => {
// Admin users - add more emails as needed
const adminEmails = [
'daveporter@oliver.agency',
// Add other admin emails here
];
return adminEmails.includes(email.toLowerCase()) ? 'admin' : 'user';
};
// Hybrid authentication middleware that handles both Azure AD and JWT tokens
const hybridAuthenticate = async (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ message: 'Access token required' });
}
try {
// Try to decode as Azure AD token first
const decoded = jwtDecode(token);
// Check if it's an Azure AD token
if (decoded.aud === AZURE_CLIENT_ID && decoded.tid === AZURE_TENANT_ID) {
return validateAzureToken(req, res, next);
} else {
// Fall back to regular JWT validation
const { authenticateToken } = require('./auth');
return authenticateToken(req, res, next);
}
} catch (error) {
// If decode fails, try regular JWT validation
const { authenticateToken } = require('./auth');
return authenticateToken(req, res, next);
}
};
module.exports = {
validateAzureToken,
hybridAuthenticate,
getAzureUserRole
};