Files
drone-detector/server/middleware/oauth-auth.js
2025-09-12 12:11:14 +02:00

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;