Fix jwt-token
This commit is contained in:
245
server/middleware/oauth-auth.js
Normal file
245
server/middleware/oauth-auth.js
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user