mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-21 02:40:14 +01:00
WIP: first pass, OpenID Proxy Auth
This commit is contained in:
parent
e90fd1df15
commit
f6925f906b
6 changed files with 317 additions and 206 deletions
50
api/server/controllers/auth/oauth.js
Normal file
50
api/server/controllers/auth/oauth.js
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
const { isEnabled } = require('@librechat/api');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { syncUserEntraGroupMemberships } = require('~/server/services/PermissionService');
|
||||||
|
const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService');
|
||||||
|
const { checkBan } = require('~/server/middleware');
|
||||||
|
|
||||||
|
const domains = {
|
||||||
|
client: process.env.DOMAIN_CLIENT,
|
||||||
|
server: process.env.DOMAIN_SERVER,
|
||||||
|
};
|
||||||
|
|
||||||
|
function createOAuthHandler(redirectUri = domains.client) {
|
||||||
|
/**
|
||||||
|
* A handler to process OAuth authentication results.
|
||||||
|
* @type {Function}
|
||||||
|
* @param {ServerRequest} req - Express request object.
|
||||||
|
* @param {ServerResponse} res - Express response object.
|
||||||
|
* @param {NextFunction} next - Express next middleware function.
|
||||||
|
*/
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (res.headersSent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkBan(req, res);
|
||||||
|
if (req.banned) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
req.user &&
|
||||||
|
req.user.provider == 'openid' &&
|
||||||
|
isEnabled(process.env.OPENID_REUSE_TOKENS) === true
|
||||||
|
) {
|
||||||
|
await syncUserEntraGroupMemberships(req.user, req.user.tokenset.access_token);
|
||||||
|
setOpenIDAuthTokens(req.user.tokenset, res, req.user._id.toString());
|
||||||
|
} else {
|
||||||
|
await setAuthTokens(req.user._id, res);
|
||||||
|
}
|
||||||
|
res.redirect(redirectUri);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error in setting authentication tokens:', err);
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createOAuthHandler,
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { loginController } = require('~/server/controllers/auth/LoginController');
|
const passport = require('passport');
|
||||||
const { getAppConfig } = require('~/server/services/Config');
|
const { randomState } = require('openid-client');
|
||||||
const { createSetBalanceConfig } = require('@librechat/api');
|
const { createSetBalanceConfig } = require('@librechat/api');
|
||||||
|
const { loginController } = require('~/server/controllers/auth/LoginController');
|
||||||
|
const { createOAuthHandler } = require('~/server/controllers/auth/oauth');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
const { getOpenIdConfig } = require('~/strategies');
|
||||||
const middleware = require('~/server/middleware');
|
const middleware = require('~/server/middleware');
|
||||||
const { Balance } = require('~/db/models');
|
const { Balance } = require('~/db/models');
|
||||||
|
|
||||||
|
|
@ -12,33 +16,51 @@ const setBalanceConfig = createSetBalanceConfig({
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Admin local authentication route - reuses main login controller
|
|
||||||
router.post(
|
router.post(
|
||||||
'/login/local',
|
'/login/local',
|
||||||
middleware.logHeaders,
|
middleware.logHeaders,
|
||||||
middleware.loginLimiter,
|
middleware.loginLimiter,
|
||||||
middleware.checkBan,
|
middleware.checkBan,
|
||||||
middleware.requireLocalAuth, // Standard local auth
|
middleware.requireLocalAuth,
|
||||||
middleware.requireAdmin, // Then check if user is admin
|
middleware.requireAdmin,
|
||||||
setBalanceConfig,
|
setBalanceConfig,
|
||||||
loginController, // Reuse existing login controller
|
loginController,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Admin token verification endpoint - simple JWT verify + admin check
|
router.get('/verify', middleware.requireJwtAuth, middleware.requireAdmin, (req, res) => {
|
||||||
router.get(
|
|
||||||
'/verify',
|
|
||||||
middleware.requireJwtAuth, // Standard JWT auth
|
|
||||||
middleware.requireAdmin, // Then check if user is admin
|
|
||||||
(req, res) => {
|
|
||||||
// Simple response - user is already verified by middleware
|
|
||||||
const { password: _p, totpSecret: _t, __v, ...user } = req.user;
|
const { password: _p, totpSecret: _t, __v, ...user } = req.user;
|
||||||
user.id = user._id.toString();
|
user.id = user._id.toString();
|
||||||
res.status(200).json({ user });
|
res.status(200).json({ user });
|
||||||
},
|
});
|
||||||
|
|
||||||
|
router.get('/oauth/openid/check', (req, res) => {
|
||||||
|
const openidConfig = getOpenIdConfig();
|
||||||
|
if (!openidConfig) {
|
||||||
|
return res.status(404).json({ message: 'OpenID configuration not found' });
|
||||||
|
}
|
||||||
|
res.status(200).json({ message: 'OpenID check successful' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/oauth/openid', (req, res, next) => {
|
||||||
|
return passport.authenticate('openidAdmin', {
|
||||||
|
session: false,
|
||||||
|
state: randomState(),
|
||||||
|
})(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/oauth/openid/callback',
|
||||||
|
passport.authenticate('openidAdmin', {
|
||||||
|
failureRedirect: `${process.env.DOMAIN_CLIENT}/oauth/error`,
|
||||||
|
failureMessage: true,
|
||||||
|
session: false,
|
||||||
|
}),
|
||||||
|
middleware.requireAdmin,
|
||||||
|
setBalanceConfig,
|
||||||
|
middleware.checkDomainAllowed,
|
||||||
|
createOAuthHandler(
|
||||||
|
(process.env.ADMIN_PANEL_URL || 'http://localhost:3000') + '/auth/openid/callback',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Future OAuth/OpenID routes will be added here
|
|
||||||
// router.get('/auth/openid', ...);
|
|
||||||
// router.get('/auth/openid/callback', ...);
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,9 @@ const passport = require('passport');
|
||||||
const { randomState } = require('openid-client');
|
const { randomState } = require('openid-client');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { ErrorTypes } = require('librechat-data-provider');
|
const { ErrorTypes } = require('librechat-data-provider');
|
||||||
const { isEnabled, createSetBalanceConfig } = require('@librechat/api');
|
const { createSetBalanceConfig } = require('@librechat/api');
|
||||||
const { checkDomainAllowed, loginLimiter, logHeaders, checkBan } = require('~/server/middleware');
|
const { checkDomainAllowed, loginLimiter, logHeaders } = require('~/server/middleware');
|
||||||
const { syncUserEntraGroupMemberships } = require('~/server/services/PermissionService');
|
const { createOAuthHandler } = require('~/server/controllers/auth/oauth');
|
||||||
const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService');
|
|
||||||
const { getAppConfig } = require('~/server/services/Config');
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { Balance } = require('~/db/models');
|
const { Balance } = require('~/db/models');
|
||||||
|
|
||||||
|
|
@ -26,32 +25,7 @@ const domains = {
|
||||||
router.use(logHeaders);
|
router.use(logHeaders);
|
||||||
router.use(loginLimiter);
|
router.use(loginLimiter);
|
||||||
|
|
||||||
const oauthHandler = async (req, res, next) => {
|
const oauthHandler = createOAuthHandler();
|
||||||
try {
|
|
||||||
if (res.headersSent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await checkBan(req, res);
|
|
||||||
if (req.banned) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
req.user &&
|
|
||||||
req.user.provider == 'openid' &&
|
|
||||||
isEnabled(process.env.OPENID_REUSE_TOKENS) === true
|
|
||||||
) {
|
|
||||||
await syncUserEntraGroupMemberships(req.user, req.user.tokenset.access_token);
|
|
||||||
setOpenIDAuthTokens(req.user.tokenset, res, req.user._id.toString());
|
|
||||||
} else {
|
|
||||||
await setAuthTokens(req.user._id, res);
|
|
||||||
}
|
|
||||||
res.redirect(domains.client);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('Error in setting authentication tokens:', err);
|
|
||||||
next(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
router.get('/error', (req, res) => {
|
router.get('/error', (req, res) => {
|
||||||
/** A single error message is pushed by passport when authentication fails. */
|
/** A single error message is pushed by passport when authentication fails. */
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
const appleLogin = require('./appleStrategy');
|
const { setupOpenId, getOpenIdConfig } = require('./openidStrategy');
|
||||||
|
const openIdJwtLogin = require('./openIdJwtStrategy');
|
||||||
|
const facebookLogin = require('./facebookStrategy');
|
||||||
|
const discordLogin = require('./discordStrategy');
|
||||||
const passportLogin = require('./localStrategy');
|
const passportLogin = require('./localStrategy');
|
||||||
const googleLogin = require('./googleStrategy');
|
const googleLogin = require('./googleStrategy');
|
||||||
const githubLogin = require('./githubStrategy');
|
const githubLogin = require('./githubStrategy');
|
||||||
const discordLogin = require('./discordStrategy');
|
|
||||||
const facebookLogin = require('./facebookStrategy');
|
|
||||||
const { setupOpenId, getOpenIdConfig } = require('./openidStrategy');
|
|
||||||
const jwtLogin = require('./jwtStrategy');
|
|
||||||
const ldapLogin = require('./ldapStrategy');
|
|
||||||
const { setupSaml } = require('./samlStrategy');
|
const { setupSaml } = require('./samlStrategy');
|
||||||
const openIdJwtLogin = require('./openIdJwtStrategy');
|
const appleLogin = require('./appleStrategy');
|
||||||
|
const ldapLogin = require('./ldapStrategy');
|
||||||
|
const jwtLogin = require('./jwtStrategy');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
appleLogin,
|
appleLogin,
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,215 @@ function convertToUsername(input, defaultValue = '') {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process OpenID authentication tokenset and userinfo
|
||||||
|
* This is the core logic extracted from the passport strategy callback
|
||||||
|
* Can be reused by both the passport strategy and proxy authentication
|
||||||
|
*
|
||||||
|
* @param {Object} tokenset - The OpenID tokenset containing access_token, id_token, etc.
|
||||||
|
* @param {Object} additionalUserinfo - Additional userinfo to merge with token claims
|
||||||
|
* @returns {Promise<Object>} The authenticated user object with tokenset
|
||||||
|
*/
|
||||||
|
async function processOpenIDAuth(tokenset, additionalUserinfo = {}) {
|
||||||
|
const claims = tokenset.claims ? tokenset.claims() : tokenset;
|
||||||
|
const userinfo = {
|
||||||
|
...claims,
|
||||||
|
...additionalUserinfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get userinfo from provider if we have access_token and haven't already
|
||||||
|
if (tokenset.access_token && !additionalUserinfo.sub) {
|
||||||
|
const providerUserinfo = await getUserInfo(openidConfig, tokenset.access_token, claims.sub);
|
||||||
|
Object.assign(userinfo, providerUserinfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appConfig = await getAppConfig();
|
||||||
|
if (!isEmailDomainAllowed(userinfo.email, appConfig?.registration?.allowedDomains)) {
|
||||||
|
logger.error(
|
||||||
|
`[OpenID Auth] Authentication blocked - email domain not allowed [Email: ${userinfo.email}]`,
|
||||||
|
);
|
||||||
|
throw new Error('Email domain not allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await findOpenIDUser({
|
||||||
|
openidId: claims.sub || userinfo.sub,
|
||||||
|
email: claims.email || userinfo.email,
|
||||||
|
strategyName: 'openidStrategy',
|
||||||
|
findUser,
|
||||||
|
});
|
||||||
|
let user = result.user;
|
||||||
|
const error = result.error;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(ErrorTypes.AUTH_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullName = getFullName(userinfo);
|
||||||
|
|
||||||
|
// Check required role if configured
|
||||||
|
const requiredRole = process.env.OPENID_REQUIRED_ROLE;
|
||||||
|
if (requiredRole) {
|
||||||
|
const requiredRoleParameterPath = process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH;
|
||||||
|
const requiredRoleTokenKind = process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND;
|
||||||
|
|
||||||
|
let decodedToken = '';
|
||||||
|
if (requiredRoleTokenKind === 'access' && tokenset.access_token) {
|
||||||
|
decodedToken = jwtDecode(tokenset.access_token);
|
||||||
|
} else if (requiredRoleTokenKind === 'id' && tokenset.id_token) {
|
||||||
|
decodedToken = jwtDecode(tokenset.id_token);
|
||||||
|
} else if (userinfo.roles) {
|
||||||
|
// If roles are already in userinfo, use them directly
|
||||||
|
const roles = Array.isArray(userinfo.roles) ? userinfo.roles : [userinfo.roles];
|
||||||
|
if (!roles.includes(requiredRole)) {
|
||||||
|
throw new Error(`You must have the "${requiredRole}" role to log in.`);
|
||||||
|
}
|
||||||
|
} else if (requiredRoleParameterPath) {
|
||||||
|
const pathParts = requiredRoleParameterPath.split('.');
|
||||||
|
let found = true;
|
||||||
|
let roles = pathParts.reduce((o, key) => {
|
||||||
|
if (o === null || o === undefined || !(key in o)) {
|
||||||
|
found = false;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return o[key];
|
||||||
|
}, decodedToken);
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
logger.error(
|
||||||
|
`[OpenID Auth] Key '${requiredRoleParameterPath}' not found in ${requiredRoleTokenKind} token!`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!roles.includes(requiredRole)) {
|
||||||
|
throw new Error(`You must have the "${requiredRole}" role to log in.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let username = '';
|
||||||
|
if (process.env.OPENID_USERNAME_CLAIM) {
|
||||||
|
username = userinfo[process.env.OPENID_USERNAME_CLAIM];
|
||||||
|
} else {
|
||||||
|
username = convertToUsername(
|
||||||
|
userinfo.preferred_username || userinfo.username || userinfo.email,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
user = {
|
||||||
|
provider: 'openid',
|
||||||
|
openidId: userinfo.sub,
|
||||||
|
username,
|
||||||
|
email: userinfo.email || '',
|
||||||
|
emailVerified: userinfo.email_verified || false,
|
||||||
|
name: fullName,
|
||||||
|
idOnTheSource: userinfo.oid,
|
||||||
|
};
|
||||||
|
|
||||||
|
const balanceConfig = getBalanceConfig(appConfig);
|
||||||
|
user = await createUser(user, balanceConfig, true, true);
|
||||||
|
} else {
|
||||||
|
user.provider = 'openid';
|
||||||
|
user.openidId = userinfo.sub;
|
||||||
|
user.username = username;
|
||||||
|
user.name = fullName;
|
||||||
|
user.idOnTheSource = userinfo.oid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle avatar
|
||||||
|
if (!!userinfo && userinfo.picture && !user.avatar?.includes('manual=true')) {
|
||||||
|
const imageUrl = userinfo.picture;
|
||||||
|
let fileName;
|
||||||
|
if (crypto) {
|
||||||
|
fileName = (await hashToken(userinfo.sub)) + '.png';
|
||||||
|
} else {
|
||||||
|
fileName = userinfo.sub + '.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageBuffer = await downloadImage(
|
||||||
|
imageUrl,
|
||||||
|
openidConfig,
|
||||||
|
tokenset.access_token,
|
||||||
|
userinfo.sub,
|
||||||
|
);
|
||||||
|
if (imageBuffer) {
|
||||||
|
const { saveBuffer } = getStrategyFunctions(
|
||||||
|
appConfig?.fileStrategy ?? process.env.CDN_PROVIDER,
|
||||||
|
);
|
||||||
|
const imagePath = await saveBuffer({
|
||||||
|
fileName,
|
||||||
|
userId: user._id.toString(),
|
||||||
|
buffer: imageBuffer,
|
||||||
|
});
|
||||||
|
user.avatar = imagePath ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user = await updateUser(user._id, user);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`[OpenID Auth] login success openidId: ${user.openidId} | email: ${user.email} | username: ${user.username}`,
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
openidId: user.openidId,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...user, tokenset };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createOpenIDCallback() {
|
||||||
|
return async (tokenset, done) => {
|
||||||
|
try {
|
||||||
|
const user = await processOpenIDAuth(tokenset);
|
||||||
|
done(null, user);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message === 'Email domain not allowed') {
|
||||||
|
return done(null, false, { message: err.message });
|
||||||
|
}
|
||||||
|
if (err.message === ErrorTypes.AUTH_FAILED) {
|
||||||
|
return done(null, false, { message: err.message });
|
||||||
|
}
|
||||||
|
if (err.message && err.message.includes('role to log in')) {
|
||||||
|
return done(null, false, { message: err.message });
|
||||||
|
}
|
||||||
|
logger.error('[openidStrategy] login failed', err);
|
||||||
|
done(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the OpenID strategy specifically for admin authentication.
|
||||||
|
* @param {Configuration} openidConfig
|
||||||
|
*/
|
||||||
|
const setupOpenIdAdmin = (openidConfig) => {
|
||||||
|
try {
|
||||||
|
if (!openidConfig) {
|
||||||
|
throw new Error('OpenID configuration not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const openidAdminLogin = new CustomOpenIDStrategy(
|
||||||
|
{
|
||||||
|
config: openidConfig,
|
||||||
|
scope: process.env.OPENID_SCOPE,
|
||||||
|
usePKCE: isEnabled(process.env.OPENID_USE_PKCE),
|
||||||
|
clockTolerance: process.env.OPENID_CLOCK_TOLERANCE || 300,
|
||||||
|
callbackURL: process.env.DOMAIN_SERVER + '/api/admin/oauth/openid/callback',
|
||||||
|
},
|
||||||
|
createOpenIDCallback(),
|
||||||
|
);
|
||||||
|
|
||||||
|
passport.use('openidAdmin', openidAdminLogin);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[openidStrategy] setupOpenIdAdmin', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the OpenID strategy for authentication.
|
* Sets up the OpenID strategy for authentication.
|
||||||
* This function configures the OpenID client, handles proxy settings,
|
* This function configures the OpenID client, handles proxy settings,
|
||||||
|
|
@ -318,10 +527,6 @@ async function setupOpenId() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const requiredRole = process.env.OPENID_REQUIRED_ROLE;
|
|
||||||
const requiredRoleParameterPath = process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH;
|
|
||||||
const requiredRoleTokenKind = process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND;
|
|
||||||
const usePKCE = isEnabled(process.env.OPENID_USE_PKCE);
|
|
||||||
logger.info(`[openidStrategy] OpenID authentication configuration`, {
|
logger.info(`[openidStrategy] OpenID authentication configuration`, {
|
||||||
generateNonce: shouldGenerateNonce,
|
generateNonce: shouldGenerateNonce,
|
||||||
reason: shouldGenerateNonce
|
reason: shouldGenerateNonce
|
||||||
|
|
@ -335,159 +540,19 @@ async function setupOpenId() {
|
||||||
scope: process.env.OPENID_SCOPE,
|
scope: process.env.OPENID_SCOPE,
|
||||||
callbackURL: process.env.DOMAIN_SERVER + process.env.OPENID_CALLBACK_URL,
|
callbackURL: process.env.DOMAIN_SERVER + process.env.OPENID_CALLBACK_URL,
|
||||||
clockTolerance: process.env.OPENID_CLOCK_TOLERANCE || 300,
|
clockTolerance: process.env.OPENID_CLOCK_TOLERANCE || 300,
|
||||||
usePKCE,
|
usePKCE: isEnabled(process.env.OPENID_USE_PKCE),
|
||||||
},
|
|
||||||
async (tokenset, done) => {
|
|
||||||
try {
|
|
||||||
const claims = tokenset.claims();
|
|
||||||
const userinfo = {
|
|
||||||
...claims,
|
|
||||||
...(await getUserInfo(openidConfig, tokenset.access_token, claims.sub)),
|
|
||||||
};
|
|
||||||
|
|
||||||
const appConfig = await getAppConfig();
|
|
||||||
if (!isEmailDomainAllowed(userinfo.email, appConfig?.registration?.allowedDomains)) {
|
|
||||||
logger.error(
|
|
||||||
`[OpenID Strategy] Authentication blocked - email domain not allowed [Email: ${userinfo.email}]`,
|
|
||||||
);
|
|
||||||
return done(null, false, { message: 'Email domain not allowed' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await findOpenIDUser({
|
|
||||||
openidId: claims.sub,
|
|
||||||
email: claims.email,
|
|
||||||
strategyName: 'openidStrategy',
|
|
||||||
findUser,
|
|
||||||
});
|
|
||||||
let user = result.user;
|
|
||||||
const error = result.error;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return done(null, false, {
|
|
||||||
message: ErrorTypes.AUTH_FAILED,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullName = getFullName(userinfo);
|
|
||||||
|
|
||||||
if (requiredRole) {
|
|
||||||
let decodedToken = '';
|
|
||||||
if (requiredRoleTokenKind === 'access') {
|
|
||||||
decodedToken = jwtDecode(tokenset.access_token);
|
|
||||||
} else if (requiredRoleTokenKind === 'id') {
|
|
||||||
decodedToken = jwtDecode(tokenset.id_token);
|
|
||||||
}
|
|
||||||
const pathParts = requiredRoleParameterPath.split('.');
|
|
||||||
let found = true;
|
|
||||||
let roles = pathParts.reduce((o, key) => {
|
|
||||||
if (o === null || o === undefined || !(key in o)) {
|
|
||||||
found = false;
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return o[key];
|
|
||||||
}, decodedToken);
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
logger.error(
|
|
||||||
`[openidStrategy] Key '${requiredRoleParameterPath}' not found in ${requiredRoleTokenKind} token!`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!roles.includes(requiredRole)) {
|
|
||||||
return done(null, false, {
|
|
||||||
message: `You must have the "${requiredRole}" role to log in.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let username = '';
|
|
||||||
if (process.env.OPENID_USERNAME_CLAIM) {
|
|
||||||
username = userinfo[process.env.OPENID_USERNAME_CLAIM];
|
|
||||||
} else {
|
|
||||||
username = convertToUsername(
|
|
||||||
userinfo.preferred_username || userinfo.username || userinfo.email,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
user = {
|
|
||||||
provider: 'openid',
|
|
||||||
openidId: userinfo.sub,
|
|
||||||
username,
|
|
||||||
email: userinfo.email || '',
|
|
||||||
emailVerified: userinfo.email_verified || false,
|
|
||||||
name: fullName,
|
|
||||||
idOnTheSource: userinfo.oid,
|
|
||||||
};
|
|
||||||
|
|
||||||
const balanceConfig = getBalanceConfig(appConfig);
|
|
||||||
user = await createUser(user, balanceConfig, true, true);
|
|
||||||
} else {
|
|
||||||
user.provider = 'openid';
|
|
||||||
user.openidId = userinfo.sub;
|
|
||||||
user.username = username;
|
|
||||||
user.name = fullName;
|
|
||||||
user.idOnTheSource = userinfo.oid;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!!userinfo && userinfo.picture && !user.avatar?.includes('manual=true')) {
|
|
||||||
/** @type {string | undefined} */
|
|
||||||
const imageUrl = userinfo.picture;
|
|
||||||
|
|
||||||
let fileName;
|
|
||||||
if (crypto) {
|
|
||||||
fileName = (await hashToken(userinfo.sub)) + '.png';
|
|
||||||
} else {
|
|
||||||
fileName = userinfo.sub + '.png';
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageBuffer = await downloadImage(
|
|
||||||
imageUrl,
|
|
||||||
openidConfig,
|
|
||||||
tokenset.access_token,
|
|
||||||
userinfo.sub,
|
|
||||||
);
|
|
||||||
if (imageBuffer) {
|
|
||||||
const { saveBuffer } = getStrategyFunctions(
|
|
||||||
appConfig?.fileStrategy ?? process.env.CDN_PROVIDER,
|
|
||||||
);
|
|
||||||
const imagePath = await saveBuffer({
|
|
||||||
fileName,
|
|
||||||
userId: user._id.toString(),
|
|
||||||
buffer: imageBuffer,
|
|
||||||
});
|
|
||||||
user.avatar = imagePath ?? '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user = await updateUser(user._id, user);
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`[openidStrategy] login success openidId: ${user.openidId} | email: ${user.email} | username: ${user.username} `,
|
|
||||||
{
|
|
||||||
user: {
|
|
||||||
openidId: user.openidId,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email,
|
|
||||||
name: user.name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
done(null, { ...user, tokenset });
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('[openidStrategy] login failed', err);
|
|
||||||
done(err);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
createOpenIDCallback(),
|
||||||
);
|
);
|
||||||
passport.use('openid', openidLogin);
|
passport.use('openid', openidLogin);
|
||||||
|
setupOpenIdAdmin(openidConfig);
|
||||||
return openidConfig;
|
return openidConfig;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('[openidStrategy]', err);
|
logger.error('[openidStrategy]', err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @function getOpenIdConfig
|
* @function getOpenIdConfig
|
||||||
* @description Returns the OpenID client instance.
|
* @description Returns the OpenID client instance.
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export default defineConfig(({ command }) => ({
|
||||||
],
|
],
|
||||||
globIgnores: ['images/**/*', '**/*.map', 'index.html'],
|
globIgnores: ['images/**/*', '**/*.map', 'index.html'],
|
||||||
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
|
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
|
||||||
navigateFallbackDenylist: [/^\/oauth/, /^\/api/],
|
navigateFallbackDenylist: [/^\/oauth/, /^\/api/, /^\/admin\/openid/],
|
||||||
},
|
},
|
||||||
includeAssets: [],
|
includeAssets: [],
|
||||||
manifest: {
|
manifest: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue