/** * OAuth 2.0/OpenID Connect Authentication Provider * Supports modern identity providers like Azure AD, Google, etc. */ const passport = require('passport'); const OAuth2Strategy = require('passport-oauth2'); const OpenIDConnectStrategy = require('passport-openidconnect'); class OAuthAuth { constructor() { this.strategies = new Map(); } /** * Configure OAuth strategy for a tenant */ configureOAuthStrategy(tenantId, config) { const strategyName = `oauth-${tenantId}`; // Determine if this is OpenID Connect or plain OAuth2 const isOpenIDConnect = config.discovery_url || config.issuer; if (isOpenIDConnect) { return this.configureOpenIDConnectStrategy(tenantId, config); } else { return this.configureOAuth2Strategy(tenantId, config); } } /** * Configure OpenID Connect strategy (recommended for modern providers) */ configureOpenIDConnectStrategy(tenantId, config) { const strategyName = `oidc-${tenantId}`; const oidcOptions = { issuer: config.issuer || config.discovery_url, clientID: config.client_id, clientSecret: config.client_secret, callbackURL: `${process.env.BASE_URL}/auth/oauth/${tenantId}/callback`, // Scopes scope: config.scopes || ['openid', 'profile', 'email'], // Claims skipUserProfile: false, // Additional parameters customHeaders: config.custom_headers || {}, passReqToCallback: true }; const strategy = new OpenIDConnectStrategy(oidcOptions, async (req, issuer, profile, accessToken, refreshToken, done) => { try { const externalUser = { id: profile.id || profile.sub, username: profile.preferred_username || profile.username || profile.email, email: profile.email, firstName: profile.given_name || profile.firstName, lastName: profile.family_name || profile.lastName, displayName: profile.name || profile.displayName, roles: profile.roles || profile['custom:roles'] || [], groups: profile.groups || profile['custom:groups'] || [], raw: profile._json // Store raw profile for custom mapping }; return done(null, { tenantId, externalUser, provider: 'oauth' }); } catch (error) { return done(error, null); } }); passport.use(strategyName, strategy); this.strategies.set(tenantId, { strategy, config, type: 'oidc' }); return strategyName; } /** * Configure OAuth 2.0 strategy (for legacy providers) */ configureOAuth2Strategy(tenantId, config) { const strategyName = `oauth2-${tenantId}`; const oauth2Options = { authorizationURL: config.authorization_url, tokenURL: config.token_url, clientID: config.client_id, clientSecret: config.client_secret, callbackURL: `${process.env.BASE_URL}/auth/oauth/${tenantId}/callback`, // Custom parameters customHeaders: config.custom_headers || {}, scope: config.scopes || ['profile', 'email'], passReqToCallback: true }; const strategy = new OAuth2Strategy(oauth2Options, async (req, accessToken, refreshToken, profile, done) => { try { // Fetch user profile from userinfo endpoint const userProfile = await this.fetchUserProfile(accessToken, config.userinfo_url); const externalUser = { id: userProfile.sub || userProfile.id, username: userProfile.preferred_username || userProfile.username || userProfile.email, email: userProfile.email, firstName: userProfile.given_name || userProfile.first_name, lastName: userProfile.family_name || userProfile.last_name, displayName: userProfile.name || userProfile.display_name, roles: userProfile.roles || [], groups: userProfile.groups || [], raw: userProfile }; return done(null, { tenantId, externalUser, provider: 'oauth' }); } catch (error) { return done(error, null); } }); passport.use(strategyName, strategy); this.strategies.set(tenantId, { strategy, config, type: 'oauth2' }); return strategyName; } /** * Fetch user profile from OAuth userinfo endpoint */ async fetchUserProfile(accessToken, userinfoUrl) { const axios = require('axios'); try { const response = await axios.get(userinfoUrl, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/json' } }); return response.data; } catch (error) { console.error('Error fetching user profile:', error); throw new Error('Failed to fetch user profile'); } } /** * Initiate OAuth authentication */ async authenticate(req, res, next) { const tenantId = req.tenant.id; // Check if strategy is configured for this tenant if (!this.strategies.has(tenantId)) { const config = req.tenant.authConfig.config; this.configureOAuthStrategy(tenantId, config); } const strategyInfo = this.strategies.get(tenantId); const strategyName = strategyInfo.type === 'oidc' ? `oidc-${tenantId}` : `oauth2-${tenantId}`; // Store return URL in session req.session.returnUrl = req.query.returnUrl || '/'; // Use passport to authenticate passport.authenticate(strategyName, { state: req.query.returnUrl || '/' })(req, res, next); } /** * Handle OAuth callback */ async handleCallback(req, res, next) { const tenantId = req.params.tenantId; const strategyInfo = this.strategies.get(tenantId); if (!strategyInfo) { return res.redirect(`/login?error=strategy_not_found&tenant=${tenantId}`); } const strategyName = strategyInfo.type === 'oidc' ? `oidc-${tenantId}` : `oauth2-${tenantId}`; passport.authenticate(strategyName, async (err, authResult) => { if (err) { console.error('OAuth authentication error:', err); return res.redirect(`/login?error=auth_failed&tenant=${tenantId}`); } if (!authResult) { return res.redirect(`/login?error=auth_cancelled&tenant=${tenantId}`); } try { const { MultiTenantAuth } = require('./multi-tenant-auth'); const multiAuth = new MultiTenantAuth(); // Create or update user const user = await multiAuth.createOrUpdateExternalUser( tenantId, authResult.externalUser, req.tenant.authConfig ); // Generate JWT token const token = multiAuth.generateJWTToken(user, tenantId); // Redirect to application with token const returnUrl = req.session.returnUrl || req.query.state || '/'; res.redirect(`${returnUrl}?token=${token}&tenant=${tenantId}`); } catch (error) { console.error('OAuth user creation error:', error); return res.redirect(`/login?error=user_creation_failed&tenant=${tenantId}`); } })(req, res, next); } /** * Get authorization URL for manual OAuth flow */ getAuthorizationUrl(tenantId, returnUrl) { const strategyInfo = this.strategies.get(tenantId); if (!strategyInfo) { throw new Error(`OAuth strategy not configured for tenant: ${tenantId}`); } const config = strategyInfo.config; const params = new URLSearchParams({ client_id: config.client_id, response_type: 'code', redirect_uri: `${process.env.BASE_URL}/auth/oauth/${tenantId}/callback`, scope: (config.scopes || ['openid', 'profile', 'email']).join(' '), state: returnUrl || '/' }); return `${config.authorization_url}?${params.toString()}`; } } module.exports = OAuthAuth;