246 lines
7.7 KiB
JavaScript
246 lines
7.7 KiB
JavaScript
/**
|
|
* 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;
|