mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
*️⃣ feat: Reuse OpenID Auth Tokens (#7397)
* feat: integrate OpenID Connect support with token reuse
- Added `jwks-rsa` and `new-openid-client` dependencies for OpenID Connect functionality.
- Implemented OpenID token refresh logic in `AuthController`.
- Enhanced `LogoutController` to handle OpenID logout and session termination.
- Updated JWT authentication middleware to support OpenID token provider.
- Modified OAuth routes to accommodate OpenID authentication and token management.
- Created `setOpenIDAuthTokens` function to manage OpenID tokens in cookies.
- Upgraded OpenID strategy with user info fetching and token exchange protocol.
- Introduced `openIdJwtLogin` strategy for handling OpenID JWT tokens.
- Added caching mechanism for exchanged OpenID tokens.
- Updated configuration to include OpenID exchanged tokens cache key.
- updated .env.example to include the new env variables needed for the feature.
* fix: update return type in downloadImage documentation for clarity and fixed openIdJwtLogin env variables
* fix: update Jest configuration and tests for OpenID strategy integration
* fix: update OpenID strategy to include callback URL in setup
* fix: fix optionalJwtAuth middleware to support OpenID token reuse and improve currentUrl method in CustomOpenIDStrategy to override the dynamic host issue related to proxy (e.g. cloudfront)
* fix: fixed code formatting
* Fix: Add mocks for openid-client and passport strategy in Jest configuration to fix unit tests
* fix eslint errors: Format mock file openid-client.
* ✨ feat: Add PKCE support for OpenID and default handling in strategy setup
---------
Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
Co-authored-by: Ruben Talstra <RubenTalstra1211@outlook.com>
This commit is contained in:
parent
d47d827ed9
commit
bf80cf30b3
24 changed files with 690 additions and 191 deletions
15
.env.example
15
.env.example
|
@ -444,6 +444,21 @@ OPENID_IMAGE_URL=
|
||||||
# This will bypass the login form completely for users, only use this if OpenID is your only authentication method
|
# This will bypass the login form completely for users, only use this if OpenID is your only authentication method
|
||||||
OPENID_AUTO_REDIRECT=false
|
OPENID_AUTO_REDIRECT=false
|
||||||
|
|
||||||
|
# Set to true to use PKCE (Proof Key for Code Exchange) for OpenID authentication
|
||||||
|
OPENID_USE_PKCE=false
|
||||||
|
#Set to true to reuse openid tokens for authentication management instead of using the mongodb session and the custom refresh token.
|
||||||
|
OPENID_REUSE_TOKENS=
|
||||||
|
#By default, signing key verification results are cached in order to prevent excessive HTTP requests to the JWKS endpoint.
|
||||||
|
#If a signing key matching the kid is found, this will be cached and the next time this kid is requested the signing key will be served from the cache.
|
||||||
|
#Default is true.
|
||||||
|
OPENID_JWKS_URL_CACHE_ENABLED=
|
||||||
|
OPENID_JWKS_URL_CACHE_TIME= # 600000 ms eq to 10 minutes leave empty to disable caching
|
||||||
|
#Set to true to trigger token exchange flow to acquire access token for the userinfo endpoint.
|
||||||
|
OPENID_ON_BEHALF_FLOW_FOR_USERINFRO_REQUIRED=
|
||||||
|
OPENID_ON_BEHALF_FLOW_USERINFRO_SCOPE = "user.read" # example for Scope Needed for Microsoft Graph API
|
||||||
|
# Set to true to use the OpenID Connect end session endpoint for logout
|
||||||
|
OPENID_USE_END_SESSION_ENDPOINT=
|
||||||
|
|
||||||
# LDAP
|
# LDAP
|
||||||
LDAP_URL=
|
LDAP_URL=
|
||||||
LDAP_BIND_DN=
|
LDAP_BIND_DN=
|
||||||
|
|
5
api/cache/getLogStores.js
vendored
5
api/cache/getLogStores.js
vendored
|
@ -61,6 +61,10 @@ const abortKeys = isRedisEnabled
|
||||||
? new Keyv({ store: keyvRedis })
|
? new Keyv({ store: keyvRedis })
|
||||||
: new Keyv({ namespace: CacheKeys.ABORT_KEYS, ttl: Time.TEN_MINUTES });
|
: new Keyv({ namespace: CacheKeys.ABORT_KEYS, ttl: Time.TEN_MINUTES });
|
||||||
|
|
||||||
|
const openIdExchangedTokensCache = isRedisEnabled
|
||||||
|
? new Keyv({ store: keyvRedis, ttl: Time.TEN_MINUTES })
|
||||||
|
: new Keyv({ namespace: CacheKeys.OPENID_EXCHANGED_TOKENS, ttl: Time.TEN_MINUTES });
|
||||||
|
|
||||||
const namespaces = {
|
const namespaces = {
|
||||||
[CacheKeys.ROLES]: roles,
|
[CacheKeys.ROLES]: roles,
|
||||||
[CacheKeys.CONFIG_STORE]: config,
|
[CacheKeys.CONFIG_STORE]: config,
|
||||||
|
@ -98,6 +102,7 @@ const namespaces = {
|
||||||
[CacheKeys.AUDIO_RUNS]: audioRuns,
|
[CacheKeys.AUDIO_RUNS]: audioRuns,
|
||||||
[CacheKeys.MESSAGES]: messages,
|
[CacheKeys.MESSAGES]: messages,
|
||||||
[CacheKeys.FLOWS]: flows,
|
[CacheKeys.FLOWS]: flows,
|
||||||
|
[CacheKeys.OPENID_EXCHANGED_TOKENS]: openIdExchangedTokensCache,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
11
api/cache/keyvRedis.js
vendored
11
api/cache/keyvRedis.js
vendored
|
@ -76,10 +76,13 @@ if (REDIS_URI && isEnabled(USE_REDIS)) {
|
||||||
keyvRedis = new KeyvRedis(REDIS_URI, keyvOpts);
|
keyvRedis = new KeyvRedis(REDIS_URI, keyvOpts);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pingInterval = setInterval(() => {
|
const pingInterval = setInterval(
|
||||||
logger.debug('KeyvRedis ping');
|
() => {
|
||||||
keyvRedis.client.ping().catch(err => logger.error('Redis keep-alive ping failed:', err));
|
logger.debug('KeyvRedis ping');
|
||||||
}, 5 * 60 * 1000);
|
keyvRedis.client.ping().catch((err) => logger.error('Redis keep-alive ping failed:', err));
|
||||||
|
},
|
||||||
|
5 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
keyvRedis.on('ready', () => {
|
keyvRedis.on('ready', () => {
|
||||||
logger.info('KeyvRedis connection ready');
|
logger.info('KeyvRedis connection ready');
|
||||||
|
|
|
@ -11,5 +11,8 @@ module.exports = {
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'~/(.*)': '<rootDir>/$1',
|
'~/(.*)': '<rootDir>/$1',
|
||||||
'~/data/auth.json': '<rootDir>/__mocks__/auth.mock.json',
|
'~/data/auth.json': '<rootDir>/__mocks__/auth.mock.json',
|
||||||
|
'^openid-client/passport$': '<rootDir>/test/__mocks__/openid-client-passport.js', // Mock for the passport strategy part
|
||||||
|
'^openid-client$': '<rootDir>/test/__mocks__/openid-client.js',
|
||||||
},
|
},
|
||||||
|
transformIgnorePatterns: ['/node_modules/(?!(openid-client|oauth4webapi|jose)/).*/'],
|
||||||
};
|
};
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"jwks-rsa": "^3.2.0",
|
||||||
"keyv": "^5.3.2",
|
"keyv": "^5.3.2",
|
||||||
"keyv-file": "^5.1.2",
|
"keyv-file": "^5.1.2",
|
||||||
"klona": "^2.0.6",
|
"klona": "^2.0.6",
|
||||||
|
@ -92,7 +93,7 @@
|
||||||
"ollama": "^0.5.0",
|
"ollama": "^0.5.0",
|
||||||
"openai": "^4.96.2",
|
"openai": "^4.96.2",
|
||||||
"openai-chat-tokens": "^0.2.8",
|
"openai-chat-tokens": "^0.2.8",
|
||||||
"openid-client": "^5.4.2",
|
"openid-client": "^6.5.0",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-apple": "^2.0.2",
|
"passport-apple": "^2.0.2",
|
||||||
"passport-discord": "^0.1.4",
|
"passport-discord": "^0.1.4",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
const openIdClient = require('openid-client');
|
||||||
const cookies = require('cookie');
|
const cookies = require('cookie');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const {
|
const {
|
||||||
|
@ -5,9 +6,12 @@ const {
|
||||||
resetPassword,
|
resetPassword,
|
||||||
setAuthTokens,
|
setAuthTokens,
|
||||||
requestPasswordReset,
|
requestPasswordReset,
|
||||||
|
setOpenIDAuthTokens,
|
||||||
} = require('~/server/services/AuthService');
|
} = require('~/server/services/AuthService');
|
||||||
const { findSession, getUserById, deleteAllUserSessions } = require('~/models');
|
const { findSession, getUserById, deleteAllUserSessions, findUser } = require('~/models');
|
||||||
|
const { getOpenIdConfig } = require('~/strategies');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
|
||||||
const registrationController = async (req, res) => {
|
const registrationController = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
@ -55,10 +59,28 @@ const resetPasswordController = async (req, res) => {
|
||||||
|
|
||||||
const refreshController = async (req, res) => {
|
const refreshController = async (req, res) => {
|
||||||
const refreshToken = req.headers.cookie ? cookies.parse(req.headers.cookie).refreshToken : null;
|
const refreshToken = req.headers.cookie ? cookies.parse(req.headers.cookie).refreshToken : null;
|
||||||
|
const token_provider = req.headers.cookie
|
||||||
|
? cookies.parse(req.headers.cookie).token_provider
|
||||||
|
: null;
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
return res.status(200).send('Refresh token not provided');
|
return res.status(200).send('Refresh token not provided');
|
||||||
}
|
}
|
||||||
|
if (token_provider === 'openid' && isEnabled(process.env.OPENID_REUSE_TOKENS) === true) {
|
||||||
|
try {
|
||||||
|
const openIdConfig = getOpenIdConfig();
|
||||||
|
const tokenset = await openIdClient.refreshTokenGrant(openIdConfig, refreshToken);
|
||||||
|
const claims = tokenset.claims();
|
||||||
|
const user = await findUser({ email: claims.email });
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).redirect('/login');
|
||||||
|
}
|
||||||
|
const token = setOpenIDAuthTokens(tokenset, res);
|
||||||
|
return res.status(200).send({ token, user });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[refreshController] OpenID token refresh error', error);
|
||||||
|
return res.status(403).send('Invalid OpenID refresh token');
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
|
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
|
||||||
const user = await getUserById(payload.id, '-password -__v -totpSecret');
|
const user = await getUserById(payload.id, '-password -__v -totpSecret');
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const cookies = require('cookie');
|
const cookies = require('cookie');
|
||||||
const { Issuer } = require('openid-client');
|
const { getOpenIdConfig } = require('~/strategies');
|
||||||
const { logoutUser } = require('~/server/services/AuthService');
|
const { logoutUser } = require('~/server/services/AuthService');
|
||||||
const { isEnabled } = require('~/server/utils');
|
const { isEnabled } = require('~/server/utils');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
@ -10,20 +10,29 @@ const logoutController = async (req, res) => {
|
||||||
const logout = await logoutUser(req, refreshToken);
|
const logout = await logoutUser(req, refreshToken);
|
||||||
const { status, message } = logout;
|
const { status, message } = logout;
|
||||||
res.clearCookie('refreshToken');
|
res.clearCookie('refreshToken');
|
||||||
|
res.clearCookie('token_provider');
|
||||||
const response = { message };
|
const response = { message };
|
||||||
if (
|
if (
|
||||||
req.user.openidId != null &&
|
req.user.openidId != null &&
|
||||||
isEnabled(process.env.OPENID_USE_END_SESSION_ENDPOINT) &&
|
isEnabled(process.env.OPENID_USE_END_SESSION_ENDPOINT) &&
|
||||||
process.env.OPENID_ISSUER
|
process.env.OPENID_ISSUER
|
||||||
) {
|
) {
|
||||||
const issuer = await Issuer.discover(process.env.OPENID_ISSUER);
|
const openIdConfig = getOpenIdConfig();
|
||||||
const redirect = issuer.metadata.end_session_endpoint;
|
if (!openIdConfig) {
|
||||||
if (!redirect) {
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
'[logoutController] end_session_endpoint not found in OpenID issuer metadata. Please verify that the issuer is correct.',
|
'[logoutController] OpenID config not found. Please verify that the open id configuration and initialization are correct.',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
response.redirect = redirect;
|
const endSessionEndpoint = openIdConfig
|
||||||
|
? openIdConfig.serverMetadata().end_session_endpoint
|
||||||
|
: null;
|
||||||
|
if (endSessionEndpoint) {
|
||||||
|
response.redirect = endSessionEndpoint;
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
'[logoutController] end_session_endpoint not found in OpenID issuer metadata. Please verify that the issuer is correct.',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res.status(status).send(response);
|
return res.status(status).send(response);
|
||||||
|
|
|
@ -75,7 +75,7 @@ const startServer = async () => {
|
||||||
|
|
||||||
/* OAUTH */
|
/* OAUTH */
|
||||||
app.use(passport.initialize());
|
app.use(passport.initialize());
|
||||||
passport.use(await jwtLogin());
|
passport.use(jwtLogin());
|
||||||
passport.use(passportLogin());
|
passport.use(passportLogin());
|
||||||
|
|
||||||
/* LDAP Auth */
|
/* LDAP Auth */
|
||||||
|
@ -84,7 +84,7 @@ const startServer = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEnabled(ALLOW_SOCIAL_LOGIN)) {
|
if (isEnabled(ALLOW_SOCIAL_LOGIN)) {
|
||||||
configureSocialLogins(app);
|
await configureSocialLogins(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use('/oauth', routes.oauth);
|
app.use('/oauth', routes.oauth);
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
|
const cookies = require('cookie');
|
||||||
|
const { isEnabled } = require('~/server/utils');
|
||||||
const passport = require('passport');
|
const passport = require('passport');
|
||||||
|
|
||||||
// This middleware does not require authentication,
|
// This middleware does not require authentication,
|
||||||
// but if the user is authenticated, it will set the user object.
|
// but if the user is authenticated, it will set the user object.
|
||||||
const optionalJwtAuth = (req, res, next) => {
|
const optionalJwtAuth = (req, res, next) => {
|
||||||
passport.authenticate('jwt', { session: false }, (err, user) => {
|
const cookieHeader = req.headers.cookie;
|
||||||
|
const tokenProvider = cookieHeader ? cookies.parse(cookieHeader).token_provider : null;
|
||||||
|
const callback = (err, user) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +15,11 @@ const optionalJwtAuth = (req, res, next) => {
|
||||||
req.user = user;
|
req.user = user;
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
})(req, res, next);
|
};
|
||||||
|
if (tokenProvider === 'openid' && isEnabled(process.env.OPENID_REUSE_TOKENS)) {
|
||||||
|
return passport.authenticate('openidJwt', { session: false }, callback)(req, res, next);
|
||||||
|
}
|
||||||
|
passport.authenticate('jwt', { session: false }, callback)(req, res, next);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = optionalJwtAuth;
|
module.exports = optionalJwtAuth;
|
||||||
|
|
|
@ -1,5 +1,23 @@
|
||||||
const passport = require('passport');
|
const passport = require('passport');
|
||||||
|
const cookies = require('cookie');
|
||||||
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
|
||||||
const requireJwtAuth = passport.authenticate('jwt', { session: false });
|
/**
|
||||||
|
* Custom Middleware to handle JWT authentication, with support for OpenID token reuse
|
||||||
|
* Switches between JWT and OpenID authentication based on cookies and environment settings
|
||||||
|
*/
|
||||||
|
const requireJwtAuth = (req, res, next) => {
|
||||||
|
// Check if token provider is specified in cookies
|
||||||
|
const cookieHeader = req.headers.cookie;
|
||||||
|
const tokenProvider = cookieHeader ? cookies.parse(cookieHeader).token_provider : null;
|
||||||
|
|
||||||
|
// Use OpenID authentication if token provider is OpenID and OPENID_REUSE_TOKENS is enabled
|
||||||
|
if (tokenProvider === 'openid' && isEnabled(process.env.OPENID_REUSE_TOKENS)) {
|
||||||
|
return passport.authenticate('openidJwt', { session: false })(req, res, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to standard JWT authentication
|
||||||
|
return passport.authenticate('jwt', { session: false })(req, res, next);
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = requireJwtAuth;
|
module.exports = requireJwtAuth;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
jest.mock('~/cache/getLogStores');
|
jest.mock('~/cache/getLogStores');
|
||||||
const request = require('supertest');
|
const request = require('supertest');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const routes = require('../');
|
const configRoute = require('../config');
|
||||||
// file deepcode ignore UseCsurfForExpress/test: test
|
// file deepcode ignore UseCsurfForExpress/test: test
|
||||||
const app = express();
|
const app = express();
|
||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
app.use('/api/config', routes.config);
|
app.use('/api/config', configRoute);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
delete process.env.APP_TITLE;
|
delete process.env.APP_TITLE;
|
||||||
|
|
|
@ -8,8 +8,9 @@ const {
|
||||||
setBalanceConfig,
|
setBalanceConfig,
|
||||||
checkDomainAllowed,
|
checkDomainAllowed,
|
||||||
} = require('~/server/middleware');
|
} = require('~/server/middleware');
|
||||||
const { setAuthTokens } = require('~/server/services/AuthService');
|
const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
@ -28,7 +29,15 @@ const oauthHandler = async (req, res) => {
|
||||||
if (req.banned) {
|
if (req.banned) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await setAuthTokens(req.user._id, res);
|
if (
|
||||||
|
req.user &&
|
||||||
|
req.user.provider == 'openid' &&
|
||||||
|
isEnabled(process.env.OPENID_REUSE_TOKENS) === true
|
||||||
|
) {
|
||||||
|
setOpenIDAuthTokens(req.user.tokenset, res);
|
||||||
|
} else {
|
||||||
|
await setAuthTokens(req.user._id, res);
|
||||||
|
}
|
||||||
res.redirect(domains.client);
|
res.redirect(domains.client);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Error in setting authentication tokens:', err);
|
logger.error('Error in setting authentication tokens:', err);
|
||||||
|
|
|
@ -377,13 +377,62 @@ const setAuthTokens = async (userId, res, sessionId = null) => {
|
||||||
secure: isProduction,
|
secure: isProduction,
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
});
|
});
|
||||||
|
res.cookie('token_provider', 'librechat', {
|
||||||
|
expires: new Date(refreshTokenExpires),
|
||||||
|
httpOnly: true,
|
||||||
|
secure: isProduction,
|
||||||
|
sameSite: 'strict',
|
||||||
|
});
|
||||||
return token;
|
return token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[setAuthTokens] Error in setting authentication tokens:', error);
|
logger.error('[setAuthTokens] Error in setting authentication tokens:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* @function setOpenIDAuthTokens
|
||||||
|
* Set OpenID Authentication Tokens
|
||||||
|
* //type tokenset from openid-client
|
||||||
|
* @param {import('openid-client').TokenEndpointResponse & import('openid-client').TokenEndpointResponseHelpers} tokenset
|
||||||
|
* - The tokenset object containing access and refresh tokens
|
||||||
|
* @param {Object} res - response object
|
||||||
|
* @returns {String} - access token
|
||||||
|
*/
|
||||||
|
const setOpenIDAuthTokens = (tokenset, res) => {
|
||||||
|
try {
|
||||||
|
if (!tokenset) {
|
||||||
|
logger.error('[setOpenIDAuthTokens] No tokenset found in request');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { REFRESH_TOKEN_EXPIRY } = process.env ?? {};
|
||||||
|
const expiryInMilliseconds = eval(REFRESH_TOKEN_EXPIRY) ?? 1000 * 60 * 60 * 24 * 7; // 7 days default
|
||||||
|
const expirationDate = new Date(Date.now() + expiryInMilliseconds);
|
||||||
|
if (tokenset == null) {
|
||||||
|
logger.error('[setOpenIDAuthTokens] No tokenset found in request');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tokenset.access_token || !tokenset.refresh_token) {
|
||||||
|
logger.error('[setOpenIDAuthTokens] No access or refresh token found in tokenset');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.cookie('refreshToken', tokenset.refresh_token, {
|
||||||
|
expires: expirationDate,
|
||||||
|
httpOnly: true,
|
||||||
|
secure: isProduction,
|
||||||
|
sameSite: 'strict',
|
||||||
|
});
|
||||||
|
res.cookie('token_provider', 'openid', {
|
||||||
|
expires: expirationDate,
|
||||||
|
httpOnly: true,
|
||||||
|
secure: isProduction,
|
||||||
|
sameSite: 'strict',
|
||||||
|
});
|
||||||
|
return tokenset.access_token;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[setOpenIDAuthTokens] Error in setting authentication tokens:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resend Verification Email
|
* Resend Verification Email
|
||||||
|
@ -452,4 +501,5 @@ module.exports = {
|
||||||
resetPassword,
|
resetPassword,
|
||||||
requestPasswordReset,
|
requestPasswordReset,
|
||||||
resendVerificationEmail,
|
resendVerificationEmail,
|
||||||
|
setOpenIDAuthTokens,
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,7 @@ const {
|
||||||
discordLogin,
|
discordLogin,
|
||||||
facebookLogin,
|
facebookLogin,
|
||||||
appleLogin,
|
appleLogin,
|
||||||
|
openIdJwtLogin,
|
||||||
} = require('~/strategies');
|
} = require('~/strategies');
|
||||||
const { isEnabled } = require('~/server/utils');
|
const { isEnabled } = require('~/server/utils');
|
||||||
const keyvRedis = require('~/cache/keyvRedis');
|
const keyvRedis = require('~/cache/keyvRedis');
|
||||||
|
@ -19,7 +20,7 @@ const { logger } = require('~/config');
|
||||||
*
|
*
|
||||||
* @param {Express.Application} app
|
* @param {Express.Application} app
|
||||||
*/
|
*/
|
||||||
const configureSocialLogins = (app) => {
|
const configureSocialLogins = async (app) => {
|
||||||
logger.info('Configuring social logins...');
|
logger.info('Configuring social logins...');
|
||||||
|
|
||||||
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
||||||
|
@ -62,8 +63,11 @@ const configureSocialLogins = (app) => {
|
||||||
}
|
}
|
||||||
app.use(session(sessionOptions));
|
app.use(session(sessionOptions));
|
||||||
app.use(passport.session());
|
app.use(passport.session());
|
||||||
setupOpenId();
|
const config = await setupOpenId();
|
||||||
|
if (isEnabled(process.env.OPENID_REUSE_TOKENS)) {
|
||||||
|
logger.info('OpenID token reuse is enabled.');
|
||||||
|
passport.use('openidJwt', openIdJwtLogin(config));
|
||||||
|
}
|
||||||
logger.info('OpenID Connect configured.');
|
logger.info('OpenID Connect configured.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,9 +4,10 @@ const googleLogin = require('./googleStrategy');
|
||||||
const githubLogin = require('./githubStrategy');
|
const githubLogin = require('./githubStrategy');
|
||||||
const discordLogin = require('./discordStrategy');
|
const discordLogin = require('./discordStrategy');
|
||||||
const facebookLogin = require('./facebookStrategy');
|
const facebookLogin = require('./facebookStrategy');
|
||||||
const setupOpenId = require('./openidStrategy');
|
const { setupOpenId, getOpenIdConfig } = require('./openidStrategy');
|
||||||
const jwtLogin = require('./jwtStrategy');
|
const jwtLogin = require('./jwtStrategy');
|
||||||
const ldapLogin = require('./ldapStrategy');
|
const ldapLogin = require('./ldapStrategy');
|
||||||
|
const openIdJwtLogin = require('./openIdJwtStrategy');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
appleLogin,
|
appleLogin,
|
||||||
|
@ -17,5 +18,7 @@ module.exports = {
|
||||||
jwtLogin,
|
jwtLogin,
|
||||||
facebookLogin,
|
facebookLogin,
|
||||||
setupOpenId,
|
setupOpenId,
|
||||||
|
getOpenIdConfig,
|
||||||
ldapLogin,
|
ldapLogin,
|
||||||
|
openIdJwtLogin,
|
||||||
};
|
};
|
|
@ -4,7 +4,7 @@ const { getUserById, updateUser } = require('~/models');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
// JWT strategy
|
// JWT strategy
|
||||||
const jwtLogin = async () =>
|
const jwtLogin = () =>
|
||||||
new JwtStrategy(
|
new JwtStrategy(
|
||||||
{
|
{
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
|
52
api/strategies/openIdJwtStrategy.js
Normal file
52
api/strategies/openIdJwtStrategy.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
const { SystemRoles } = require('librechat-data-provider');
|
||||||
|
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
|
||||||
|
const { updateUser, findUser } = require('~/models');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
const jwksRsa = require('jwks-rsa');
|
||||||
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
/**
|
||||||
|
* @function openIdJwtLogin
|
||||||
|
* @param {import('openid-client').Configuration} openIdConfig - Configuration object for the JWT strategy.
|
||||||
|
* @returns {JwtStrategy}
|
||||||
|
* @description This function creates a JWT strategy for OpenID authentication.
|
||||||
|
* It uses the jwks-rsa library to retrieve the signing key from a JWKS endpoint.
|
||||||
|
* The strategy extracts the JWT from the Authorization header as a Bearer token.
|
||||||
|
* The JWT is then verified using the signing key, and the user is retrieved from the database.
|
||||||
|
*/
|
||||||
|
const openIdJwtLogin = (openIdConfig) =>
|
||||||
|
new JwtStrategy(
|
||||||
|
{
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
secretOrKeyProvider: jwksRsa.passportJwtSecret({
|
||||||
|
cache: isEnabled(process.env.OPENID_JWKS_URL_CACHE_ENABLED) || true,
|
||||||
|
cacheMaxAge: process.env.OPENID_JWKS_URL_CACHE_TIME
|
||||||
|
? eval(process.env.OPENID_JWKS_URL_CACHE_TIME)
|
||||||
|
: 60000,
|
||||||
|
jwksUri: openIdConfig.serverMetadata().jwks_uri,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async (payload, done) => {
|
||||||
|
try {
|
||||||
|
const user = await findUser({ openidId: payload?.sub });
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
user.id = user._id.toString();
|
||||||
|
if (!user.role) {
|
||||||
|
user.role = SystemRoles.USER;
|
||||||
|
await updateUser(user.id, { role: user.role });
|
||||||
|
}
|
||||||
|
done(null, user);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
'[openIdJwtLogin] openId JwtStrategy => no user found with the sub claims: ' +
|
||||||
|
payload?.sub,
|
||||||
|
);
|
||||||
|
done(null, false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
done(err, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = openIdJwtLogin;
|
|
@ -1,28 +1,101 @@
|
||||||
|
const { CacheKeys } = require('librechat-data-provider');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const passport = require('passport');
|
const passport = require('passport');
|
||||||
const jwtDecode = require('jsonwebtoken/decode');
|
const jwtDecode = require('jsonwebtoken/decode');
|
||||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||||
const { Issuer, Strategy: OpenIDStrategy, custom } = require('openid-client');
|
const client = require('openid-client');
|
||||||
|
const { Strategy: OpenIDStrategy } = require('openid-client/passport');
|
||||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||||
const { findUser, createUser, updateUser } = require('~/models/userMethods');
|
const { findUser, createUser, updateUser } = require('~/models/userMethods');
|
||||||
const { hashToken } = require('~/server/utils/crypto');
|
const { hashToken } = require('~/server/utils/crypto');
|
||||||
const { isEnabled } = require('~/server/utils');
|
const { isEnabled } = require('~/server/utils');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
const getLogStores = require('~/cache/getLogStores');
|
||||||
|
|
||||||
let crypto;
|
/**
|
||||||
try {
|
* @typedef {import('openid-client').ClientMetadata} ClientMetadata
|
||||||
crypto = require('node:crypto');
|
* @typedef {import('openid-client').Configuration} Configuration
|
||||||
} catch (err) {
|
**/
|
||||||
logger.error('[openidStrategy] crypto support is disabled!', err);
|
|
||||||
|
/** @typedef {Configuration | null} */
|
||||||
|
let openidConfig = null;
|
||||||
|
|
||||||
|
//overload currenturl function because of express version 4 buggy req.host doesn't include port
|
||||||
|
//More info https://github.com/panva/openid-client/pull/713
|
||||||
|
|
||||||
|
class CustomOpenIDStrategy extends OpenIDStrategy {
|
||||||
|
currentUrl(req) {
|
||||||
|
const hostAndProtocol = process.env.DOMAIN_SERVER;
|
||||||
|
return new URL(`${hostAndProtocol}${req.originalUrl ?? req.url}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exchange the access token for a new access token using the on-behalf-of flow if required.
|
||||||
|
* @param {Configuration} config
|
||||||
|
* @param {string} accessToken access token to be exchanged if necessary
|
||||||
|
* @param {string} sub - The subject identifier of the user. usually found as "sub" in the claims of the token
|
||||||
|
* @param {boolean} fromCache - Indicates whether to use cached tokens.
|
||||||
|
* @returns {Promise<string>} The new access token if exchanged, otherwise the original access token.
|
||||||
|
*/
|
||||||
|
const exchangeAccessTokenIfNeeded = async (config, accessToken, sub, fromCache = false) => {
|
||||||
|
const tokensCache = getLogStores(CacheKeys.OPENID_EXCHANGED_TOKENS);
|
||||||
|
const onBehalfFlowRequired = isEnabled(process.env.OPENID_ON_BEHALF_FLOW_FOR_USERINFRO_REQUIRED);
|
||||||
|
if (onBehalfFlowRequired) {
|
||||||
|
if (fromCache) {
|
||||||
|
const cachedToken = await tokensCache.get(sub);
|
||||||
|
if (cachedToken) {
|
||||||
|
return cachedToken.access_token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const grantResponse = await client.genericGrantRequest(
|
||||||
|
config,
|
||||||
|
'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||||
|
{
|
||||||
|
scope: process.env.OPENID_ON_BEHALF_FLOW_USERINFRO_SCOPE || 'user.read',
|
||||||
|
assertion: accessToken,
|
||||||
|
requested_token_use: 'on_behalf_of',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await tokensCache.set(
|
||||||
|
sub,
|
||||||
|
{
|
||||||
|
access_token: grantResponse.access_token,
|
||||||
|
},
|
||||||
|
grantResponse.expires_in * 1000,
|
||||||
|
);
|
||||||
|
return grantResponse.access_token;
|
||||||
|
}
|
||||||
|
return accessToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get user info from openid provider
|
||||||
|
* @param {Configuration} config
|
||||||
|
* @param {string} accessToken access token
|
||||||
|
* @param {string} sub - The subject identifier of the user. usually found as "sub" in the claims of the token
|
||||||
|
* @returns {Promise<Object|null>}
|
||||||
|
*/
|
||||||
|
const getUserInfo = async (config, accessToken, sub) => {
|
||||||
|
try {
|
||||||
|
const exchangedAccessToken = await exchangeAccessTokenIfNeeded(config, accessToken, sub);
|
||||||
|
return await client.fetchUserInfo(config, exchangedAccessToken, sub);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`[openidStrategy] getUserInfo: Error fetching user info: ${error}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads an image from a URL using an access token.
|
* Downloads an image from a URL using an access token.
|
||||||
* @param {string} url
|
* @param {string} url
|
||||||
* @param {string} accessToken
|
* @param {Configuration} config
|
||||||
* @returns {Promise<Buffer>}
|
* @param {string} accessToken access token
|
||||||
|
* @param {string} sub - The subject identifier of the user. usually found as "sub" in the claims of the token
|
||||||
|
* @returns {Promise<Buffer | string>} The image buffer or an empty string if the download fails.
|
||||||
*/
|
*/
|
||||||
const downloadImage = async (url, accessToken) => {
|
const downloadImage = async (url, config, accessToken, sub) => {
|
||||||
|
const exchangedAccessToken = await exchangeAccessTokenIfNeeded(config, accessToken, sub, true);
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
@ -31,7 +104,7 @@ const downloadImage = async (url, accessToken) => {
|
||||||
const options = {
|
const options = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${exchangedAccessToken}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -105,63 +178,68 @@ function convertToUsername(input, defaultValue = '') {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the OpenID strategy for authentication.
|
||||||
|
* This function configures the OpenID client, handles proxy settings,
|
||||||
|
* and defines the OpenID strategy for Passport.js.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @function setupOpenId
|
||||||
|
* @returns {Promise<Configuration | null>} A promise that resolves when the OpenID strategy is set up and returns the openid client config object.
|
||||||
|
* @throws {Error} If an error occurs during the setup process.
|
||||||
|
*/
|
||||||
async function setupOpenId() {
|
async function setupOpenId() {
|
||||||
try {
|
try {
|
||||||
if (process.env.PROXY) {
|
/** @type {ClientMetadata} */
|
||||||
const proxyAgent = new HttpsProxyAgent(process.env.PROXY);
|
|
||||||
custom.setHttpOptionsDefaults({
|
|
||||||
agent: proxyAgent,
|
|
||||||
});
|
|
||||||
logger.info(`[openidStrategy] proxy agent added: ${process.env.PROXY}`);
|
|
||||||
}
|
|
||||||
const issuer = await Issuer.discover(process.env.OPENID_ISSUER);
|
|
||||||
/* Supported Algorithms, openid-client v5 doesn't set it automatically as discovered from server.
|
|
||||||
- id_token_signed_response_alg // defaults to 'RS256'
|
|
||||||
- request_object_signing_alg // defaults to 'RS256'
|
|
||||||
- userinfo_signed_response_alg // not in v5
|
|
||||||
- introspection_signed_response_alg // not in v5
|
|
||||||
- authorization_signed_response_alg // not in v5
|
|
||||||
*/
|
|
||||||
/** @type {import('openid-client').ClientMetadata} */
|
|
||||||
const clientMetadata = {
|
const clientMetadata = {
|
||||||
client_id: process.env.OPENID_CLIENT_ID,
|
client_id: process.env.OPENID_CLIENT_ID,
|
||||||
client_secret: process.env.OPENID_CLIENT_SECRET,
|
client_secret: process.env.OPENID_CLIENT_SECRET,
|
||||||
redirect_uris: [process.env.DOMAIN_SERVER + process.env.OPENID_CALLBACK_URL],
|
|
||||||
};
|
};
|
||||||
if (isEnabled(process.env.OPENID_SET_FIRST_SUPPORTED_ALGORITHM)) {
|
|
||||||
clientMetadata.id_token_signed_response_alg =
|
/** @type {Configuration} */
|
||||||
issuer.id_token_signing_alg_values_supported?.[0] || 'RS256';
|
openidConfig = await client.discovery(
|
||||||
|
new URL(process.env.OPENID_ISSUER),
|
||||||
|
process.env.OPENID_CLIENT_ID,
|
||||||
|
clientMetadata,
|
||||||
|
);
|
||||||
|
if (process.env.PROXY) {
|
||||||
|
const proxyAgent = new HttpsProxyAgent(process.env.PROXY);
|
||||||
|
openidConfig[client.customFetch] = (...args) => {
|
||||||
|
return fetch(args[0], { ...args[1], agent: proxyAgent });
|
||||||
|
};
|
||||||
|
logger.info(`[openidStrategy] proxy agent added: ${process.env.PROXY}`);
|
||||||
}
|
}
|
||||||
const client = new issuer.Client(clientMetadata);
|
|
||||||
const requiredRole = process.env.OPENID_REQUIRED_ROLE;
|
const requiredRole = process.env.OPENID_REQUIRED_ROLE;
|
||||||
const requiredRoleParameterPath = process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH;
|
const requiredRoleParameterPath = process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH;
|
||||||
const requiredRoleTokenKind = process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND;
|
const requiredRoleTokenKind = process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND;
|
||||||
const openidLogin = new OpenIDStrategy(
|
const usePKCE = isEnabled(process.env.OPENID_USE_PKCE);
|
||||||
|
const openidLogin = new CustomOpenIDStrategy(
|
||||||
{
|
{
|
||||||
client,
|
config: openidConfig,
|
||||||
params: {
|
scope: process.env.OPENID_SCOPE,
|
||||||
scope: process.env.OPENID_SCOPE,
|
callbackURL: process.env.DOMAIN_SERVER + process.env.OPENID_CALLBACK_URL,
|
||||||
},
|
usePKCE,
|
||||||
},
|
},
|
||||||
async (tokenset, userinfo, done) => {
|
async (tokenset, done) => {
|
||||||
try {
|
try {
|
||||||
logger.info(`[openidStrategy] verify login openidId: ${userinfo.sub}`);
|
const claims = tokenset.claims();
|
||||||
logger.debug('[openidStrategy] very login tokenset and userinfo', { tokenset, userinfo });
|
let user = await findUser({ openidId: claims.sub });
|
||||||
|
|
||||||
let user = await findUser({ openidId: userinfo.sub });
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`[openidStrategy] user ${user ? 'found' : 'not found'} with openidId: ${userinfo.sub}`,
|
`[openidStrategy] user ${user ? 'found' : 'not found'} with openidId: ${claims.sub}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await findUser({ email: userinfo.email });
|
user = await findUser({ email: claims.email });
|
||||||
logger.info(
|
logger.info(
|
||||||
`[openidStrategy] user ${user ? 'found' : 'not found'} with email: ${
|
`[openidStrategy] user ${user ? 'found' : 'not found'} with email: ${
|
||||||
userinfo.email
|
claims.email
|
||||||
} for openidId: ${userinfo.sub}`,
|
} for openidId: ${claims.sub}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const userinfo = {
|
||||||
|
...claims,
|
||||||
|
...(await getUserInfo(openidConfig, tokenset.access_token, claims.sub)),
|
||||||
|
};
|
||||||
const fullName = getFullName(userinfo);
|
const fullName = getFullName(userinfo);
|
||||||
|
|
||||||
if (requiredRole) {
|
if (requiredRole) {
|
||||||
|
@ -220,7 +298,7 @@ async function setupOpenId() {
|
||||||
user.name = fullName;
|
user.name = fullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userinfo.picture && !user.avatar?.includes('manual=true')) {
|
if (!!userinfo && userinfo.picture && !user.avatar?.includes('manual=true')) {
|
||||||
/** @type {string | undefined} */
|
/** @type {string | undefined} */
|
||||||
const imageUrl = userinfo.picture;
|
const imageUrl = userinfo.picture;
|
||||||
|
|
||||||
|
@ -231,7 +309,12 @@ async function setupOpenId() {
|
||||||
fileName = userinfo.sub + '.png';
|
fileName = userinfo.sub + '.png';
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageBuffer = await downloadImage(imageUrl, tokenset.access_token);
|
const imageBuffer = await downloadImage(
|
||||||
|
imageUrl,
|
||||||
|
openidConfig,
|
||||||
|
tokenset.access_token,
|
||||||
|
userinfo.sub,
|
||||||
|
);
|
||||||
if (imageBuffer) {
|
if (imageBuffer) {
|
||||||
const { saveBuffer } = getStrategyFunctions(process.env.CDN_PROVIDER);
|
const { saveBuffer } = getStrategyFunctions(process.env.CDN_PROVIDER);
|
||||||
const imagePath = await saveBuffer({
|
const imagePath = await saveBuffer({
|
||||||
|
@ -257,18 +340,34 @@ async function setupOpenId() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
done(null, user);
|
done(null, { ...user, tokenset });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('[openidStrategy] login failed', err);
|
logger.error('[openidStrategy] login failed', err);
|
||||||
done(err);
|
done(err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
passport.use('openid', openidLogin);
|
passport.use('openid', openidLogin);
|
||||||
|
return openidConfig;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('[openidStrategy]', err);
|
logger.error('[openidStrategy]', err);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @function getOpenIdConfig
|
||||||
|
* @description Returns the OpenID client instance.
|
||||||
|
* @throws {Error} If the OpenID client is not initialized.
|
||||||
|
* @returns {Configuration}
|
||||||
|
*/
|
||||||
|
function getOpenIdConfig() {
|
||||||
|
if (!openidConfig) {
|
||||||
|
throw new Error('OpenID client is not initialized. Please call setupOpenId first.');
|
||||||
|
}
|
||||||
|
return openidConfig;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = setupOpenId;
|
module.exports = {
|
||||||
|
setupOpenId,
|
||||||
|
getOpenIdConfig,
|
||||||
|
};
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const jwtDecode = require('jsonwebtoken/decode');
|
const jwtDecode = require('jsonwebtoken/decode');
|
||||||
const { Issuer, Strategy: OpenIDStrategy } = require('openid-client');
|
|
||||||
const { findUser, createUser, updateUser } = require('~/models/userMethods');
|
const { findUser, createUser, updateUser } = require('~/models/userMethods');
|
||||||
const setupOpenId = require('./openidStrategy');
|
const { setupOpenId } = require('./openidStrategy');
|
||||||
|
|
||||||
// --- Mocks ---
|
// --- Mocks ---
|
||||||
jest.mock('node-fetch');
|
jest.mock('node-fetch');
|
||||||
jest.mock('openid-client');
|
|
||||||
jest.mock('jsonwebtoken/decode');
|
jest.mock('jsonwebtoken/decode');
|
||||||
jest.mock('~/server/services/Files/strategies', () => ({
|
jest.mock('~/server/services/Files/strategies', () => ({
|
||||||
getStrategyFunctions: jest.fn(() => ({
|
getStrategyFunctions: jest.fn(() => ({
|
||||||
// You can modify this mock as needed (here returning a dummy function)
|
|
||||||
saveBuffer: jest.fn().mockResolvedValue('/fake/path/to/avatar.png'),
|
saveBuffer: jest.fn().mockResolvedValue('/fake/path/to/avatar.png'),
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
@ -23,38 +20,73 @@ jest.mock('~/server/utils/crypto', () => ({
|
||||||
hashToken: jest.fn().mockResolvedValue('hashed-token'),
|
hashToken: jest.fn().mockResolvedValue('hashed-token'),
|
||||||
}));
|
}));
|
||||||
jest.mock('~/server/utils', () => ({
|
jest.mock('~/server/utils', () => ({
|
||||||
isEnabled: jest.fn(() => false), // default to false, override per test if needed
|
isEnabled: jest.fn(() => false),
|
||||||
}));
|
}));
|
||||||
jest.mock('~/config', () => ({
|
jest.mock('~/config', () => ({
|
||||||
logger: {
|
logger: {
|
||||||
info: jest.fn(),
|
info: jest.fn(),
|
||||||
debug: jest.fn(),
|
debug: jest.fn(),
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
jest.mock('~/cache/getLogStores', () =>
|
||||||
|
jest.fn(() => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
set: jest.fn(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
jest.mock('librechat-data-provider', () => ({
|
||||||
|
CacheKeys: {
|
||||||
|
OPENID_EXCHANGED_TOKENS: 'openid-exchanged-tokens',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock Issuer.discover so that setupOpenId gets a fake issuer and client
|
// Mock the openid-client module and all its dependencies
|
||||||
Issuer.discover = jest.fn().mockResolvedValue({
|
jest.mock('openid-client', () => {
|
||||||
id_token_signing_alg_values_supported: ['RS256'],
|
return {
|
||||||
Client: jest.fn().mockImplementation((clientMetadata) => {
|
discovery: jest.fn().mockResolvedValue({
|
||||||
return {
|
clientId: 'fake_client_id',
|
||||||
metadata: clientMetadata,
|
clientSecret: 'fake_client_secret',
|
||||||
};
|
issuer: 'https://fake-issuer.com',
|
||||||
}),
|
// Add any other properties needed by the implementation
|
||||||
|
}),
|
||||||
|
fetchUserInfo: jest.fn().mockImplementation((config, accessToken, sub) => {
|
||||||
|
// Only return additional properties, but don't override any claims
|
||||||
|
return Promise.resolve({
|
||||||
|
preferred_username: 'preferred_username',
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
customFetch: Symbol('customFetch'),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// To capture the verify callback from the strategy, we grab it from the mock constructor
|
jest.mock('openid-client/passport', () => {
|
||||||
let verifyCallback;
|
let verifyCallback;
|
||||||
OpenIDStrategy.mockImplementation((options, verify) => {
|
const mockStrategy = jest.fn((options, verify) => {
|
||||||
verifyCallback = verify;
|
verifyCallback = verify;
|
||||||
return { name: 'openid', options, verify };
|
return { name: 'openid', options, verify };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
Strategy: mockStrategy,
|
||||||
|
__getVerifyCallback: () => verifyCallback,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mock passport
|
||||||
|
jest.mock('passport', () => ({
|
||||||
|
use: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('setupOpenId', () => {
|
describe('setupOpenId', () => {
|
||||||
|
// Store a reference to the verify callback once it's set up
|
||||||
|
let verifyCallback;
|
||||||
|
|
||||||
// Helper to wrap the verify callback in a promise
|
// Helper to wrap the verify callback in a promise
|
||||||
const validate = (tokenset, userinfo) =>
|
const validate = (tokenset) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
verifyCallback(tokenset, userinfo, (err, user, details) => {
|
verifyCallback(tokenset, (err, user, details) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
|
@ -66,17 +98,16 @@ describe('setupOpenId', () => {
|
||||||
const tokenset = {
|
const tokenset = {
|
||||||
id_token: 'fake_id_token',
|
id_token: 'fake_id_token',
|
||||||
access_token: 'fake_access_token',
|
access_token: 'fake_access_token',
|
||||||
};
|
claims: () => ({
|
||||||
|
sub: '1234',
|
||||||
const baseUserinfo = {
|
email: 'test@example.com',
|
||||||
sub: '1234',
|
email_verified: true,
|
||||||
email: 'test@example.com',
|
given_name: 'First',
|
||||||
email_verified: true,
|
family_name: 'Last',
|
||||||
given_name: 'First',
|
name: 'My Full',
|
||||||
family_name: 'Last',
|
username: 'flast',
|
||||||
name: 'My Full',
|
picture: 'https://example.com/avatar.png',
|
||||||
username: 'flast',
|
}),
|
||||||
picture: 'https://example.com/avatar.png',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
@ -96,6 +127,7 @@ describe('setupOpenId', () => {
|
||||||
delete process.env.OPENID_USERNAME_CLAIM;
|
delete process.env.OPENID_USERNAME_CLAIM;
|
||||||
delete process.env.OPENID_NAME_CLAIM;
|
delete process.env.OPENID_NAME_CLAIM;
|
||||||
delete process.env.PROXY;
|
delete process.env.PROXY;
|
||||||
|
delete process.env.OPENID_USE_PKCE;
|
||||||
|
|
||||||
// Default jwtDecode mock returns a token that includes the required role.
|
// Default jwtDecode mock returns a token that includes the required role.
|
||||||
jwtDecode.mockReturnValue({
|
jwtDecode.mockReturnValue({
|
||||||
|
@ -120,16 +152,17 @@ describe('setupOpenId', () => {
|
||||||
};
|
};
|
||||||
fetch.mockResolvedValue(fakeResponse);
|
fetch.mockResolvedValue(fakeResponse);
|
||||||
|
|
||||||
// Finally, call the setup function so that passport.use gets called
|
// Call the setup function and capture the verify callback
|
||||||
await setupOpenId();
|
await setupOpenId();
|
||||||
|
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a new user with correct username when username claim exists', async () => {
|
it('should create a new user with correct username when username claim exists', async () => {
|
||||||
// Arrange – our userinfo already has username 'flast'
|
// Arrange – our userinfo already has username 'flast'
|
||||||
const userinfo = { ...baseUserinfo };
|
const userinfo = tokenset.claims();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { user } = await validate(tokenset, userinfo);
|
const { user } = await validate(tokenset);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(user.username).toBe(userinfo.username);
|
expect(user.username).toBe(userinfo.username);
|
||||||
|
@ -148,13 +181,13 @@ describe('setupOpenId', () => {
|
||||||
|
|
||||||
it('should use given_name as username when username claim is missing', async () => {
|
it('should use given_name as username when username claim is missing', async () => {
|
||||||
// Arrange – remove username from userinfo
|
// Arrange – remove username from userinfo
|
||||||
const userinfo = { ...baseUserinfo };
|
const userinfo = { ...tokenset.claims() };
|
||||||
delete userinfo.username;
|
delete userinfo.username;
|
||||||
// Expect the username to be the given name (unchanged case)
|
// Expect the username to be the given name (unchanged case)
|
||||||
const expectUsername = userinfo.given_name;
|
const expectUsername = userinfo.given_name;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { user } = await validate(tokenset, userinfo);
|
const { user } = await validate({ ...tokenset, claims: () => userinfo });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(user.username).toBe(expectUsername);
|
expect(user.username).toBe(expectUsername);
|
||||||
|
@ -167,13 +200,13 @@ describe('setupOpenId', () => {
|
||||||
|
|
||||||
it('should use email as username when username and given_name are missing', async () => {
|
it('should use email as username when username and given_name are missing', async () => {
|
||||||
// Arrange – remove username and given_name
|
// Arrange – remove username and given_name
|
||||||
const userinfo = { ...baseUserinfo };
|
const userinfo = { ...tokenset.claims() };
|
||||||
delete userinfo.username;
|
delete userinfo.username;
|
||||||
delete userinfo.given_name;
|
delete userinfo.given_name;
|
||||||
const expectUsername = userinfo.email;
|
const expectUsername = userinfo.email;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { user } = await validate(tokenset, userinfo);
|
const { user } = await validate({ ...tokenset, claims: () => userinfo });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(user.username).toBe(expectUsername);
|
expect(user.username).toBe(expectUsername);
|
||||||
|
@ -187,10 +220,10 @@ describe('setupOpenId', () => {
|
||||||
it('should override username with OPENID_USERNAME_CLAIM when set', async () => {
|
it('should override username with OPENID_USERNAME_CLAIM when set', async () => {
|
||||||
// Arrange – set OPENID_USERNAME_CLAIM so that the sub claim is used
|
// Arrange – set OPENID_USERNAME_CLAIM so that the sub claim is used
|
||||||
process.env.OPENID_USERNAME_CLAIM = 'sub';
|
process.env.OPENID_USERNAME_CLAIM = 'sub';
|
||||||
const userinfo = { ...baseUserinfo };
|
const userinfo = tokenset.claims();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { user } = await validate(tokenset, userinfo);
|
const { user } = await validate(tokenset);
|
||||||
|
|
||||||
// Assert – username should equal the sub (converted as-is)
|
// Assert – username should equal the sub (converted as-is)
|
||||||
expect(user.username).toBe(userinfo.sub);
|
expect(user.username).toBe(userinfo.sub);
|
||||||
|
@ -203,11 +236,11 @@ describe('setupOpenId', () => {
|
||||||
|
|
||||||
it('should set the full name correctly when given_name and family_name exist', async () => {
|
it('should set the full name correctly when given_name and family_name exist', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const userinfo = { ...baseUserinfo };
|
const userinfo = tokenset.claims();
|
||||||
const expectedFullName = `${userinfo.given_name} ${userinfo.family_name}`;
|
const expectedFullName = `${userinfo.given_name} ${userinfo.family_name}`;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { user } = await validate(tokenset, userinfo);
|
const { user } = await validate(tokenset);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(user.name).toBe(expectedFullName);
|
expect(user.name).toBe(expectedFullName);
|
||||||
|
@ -216,10 +249,10 @@ describe('setupOpenId', () => {
|
||||||
it('should override full name with OPENID_NAME_CLAIM when set', async () => {
|
it('should override full name with OPENID_NAME_CLAIM when set', async () => {
|
||||||
// Arrange – use the name claim as the full name
|
// Arrange – use the name claim as the full name
|
||||||
process.env.OPENID_NAME_CLAIM = 'name';
|
process.env.OPENID_NAME_CLAIM = 'name';
|
||||||
const userinfo = { ...baseUserinfo, name: 'Custom Name' };
|
const userinfo = { ...tokenset.claims(), name: 'Custom Name' };
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { user } = await validate(tokenset, userinfo);
|
const { user } = await validate({ ...tokenset, claims: () => userinfo });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(user.name).toBe('Custom Name');
|
expect(user.name).toBe('Custom Name');
|
||||||
|
@ -230,31 +263,31 @@ describe('setupOpenId', () => {
|
||||||
const existingUser = {
|
const existingUser = {
|
||||||
_id: 'existingUserId',
|
_id: 'existingUserId',
|
||||||
provider: 'local',
|
provider: 'local',
|
||||||
email: baseUserinfo.email,
|
email: tokenset.claims().email,
|
||||||
openidId: '',
|
openidId: '',
|
||||||
username: '',
|
username: '',
|
||||||
name: '',
|
name: '',
|
||||||
};
|
};
|
||||||
findUser.mockImplementation(async (query) => {
|
findUser.mockImplementation(async (query) => {
|
||||||
if (query.openidId === baseUserinfo.sub || query.email === baseUserinfo.email) {
|
if (query.openidId === tokenset.claims().sub || query.email === tokenset.claims().email) {
|
||||||
return existingUser;
|
return existingUser;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const userinfo = { ...baseUserinfo };
|
const userinfo = tokenset.claims();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await validate(tokenset, userinfo);
|
await validate(tokenset);
|
||||||
|
|
||||||
// Assert – updateUser should be called and the user object updated
|
// Assert – updateUser should be called and the user object updated
|
||||||
expect(updateUser).toHaveBeenCalledWith(
|
expect(updateUser).toHaveBeenCalledWith(
|
||||||
existingUser._id,
|
existingUser._id,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
provider: 'openid',
|
provider: 'openid',
|
||||||
openidId: baseUserinfo.sub,
|
openidId: userinfo.sub,
|
||||||
username: baseUserinfo.username,
|
username: userinfo.username,
|
||||||
name: `${baseUserinfo.given_name} ${baseUserinfo.family_name}`,
|
name: `${userinfo.given_name} ${userinfo.family_name}`,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -264,10 +297,10 @@ describe('setupOpenId', () => {
|
||||||
jwtDecode.mockReturnValue({
|
jwtDecode.mockReturnValue({
|
||||||
roles: ['SomeOtherRole'],
|
roles: ['SomeOtherRole'],
|
||||||
});
|
});
|
||||||
const userinfo = { ...baseUserinfo };
|
const userinfo = tokenset.claims();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { user, details } = await validate(tokenset, userinfo);
|
const { user, details } = await validate(tokenset);
|
||||||
|
|
||||||
// Assert – verify that the strategy rejects login
|
// Assert – verify that the strategy rejects login
|
||||||
expect(user).toBe(false);
|
expect(user).toBe(false);
|
||||||
|
@ -276,10 +309,10 @@ describe('setupOpenId', () => {
|
||||||
|
|
||||||
it('should attempt to download and save the avatar if picture is provided', async () => {
|
it('should attempt to download and save the avatar if picture is provided', async () => {
|
||||||
// Arrange – ensure userinfo contains a picture URL
|
// Arrange – ensure userinfo contains a picture URL
|
||||||
const userinfo = { ...baseUserinfo };
|
const userinfo = tokenset.claims();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { user } = await validate(tokenset, userinfo);
|
const { user } = await validate(tokenset);
|
||||||
|
|
||||||
// Assert – verify that download was attempted and the avatar field was set via updateUser
|
// Assert – verify that download was attempted and the avatar field was set via updateUser
|
||||||
expect(fetch).toHaveBeenCalled();
|
expect(fetch).toHaveBeenCalled();
|
||||||
|
@ -289,14 +322,25 @@ describe('setupOpenId', () => {
|
||||||
|
|
||||||
it('should not attempt to download avatar if picture is not provided', async () => {
|
it('should not attempt to download avatar if picture is not provided', async () => {
|
||||||
// Arrange – remove picture
|
// Arrange – remove picture
|
||||||
const userinfo = { ...baseUserinfo };
|
const userinfo = { ...tokenset.claims() };
|
||||||
delete userinfo.picture;
|
delete userinfo.picture;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await validate(tokenset, userinfo);
|
await validate({ ...tokenset, claims: () => userinfo });
|
||||||
|
|
||||||
// Assert – fetch should not be called and avatar should remain undefined or empty
|
// Assert – fetch should not be called and avatar should remain undefined or empty
|
||||||
expect(fetch).not.toHaveBeenCalled();
|
expect(fetch).not.toHaveBeenCalled();
|
||||||
// Depending on your implementation, user.avatar may be undefined or an empty string.
|
// Depending on your implementation, user.avatar may be undefined or an empty string.
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should default to usePKCE false when OPENID_USE_PKCE is not defined', async () => {
|
||||||
|
const OpenIDStrategy = require('openid-client/passport').Strategy;
|
||||||
|
|
||||||
|
delete process.env.OPENID_USE_PKCE;
|
||||||
|
await setupOpenId();
|
||||||
|
|
||||||
|
const callOptions = OpenIDStrategy.mock.calls[OpenIDStrategy.mock.calls.length - 1][0];
|
||||||
|
expect(callOptions.usePKCE).toBe(false);
|
||||||
|
expect(callOptions.params?.code_challenge_method).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,7 +7,8 @@ const socialLogin =
|
||||||
(provider, getProfileDetails) => async (accessToken, refreshToken, idToken, profile, cb) => {
|
(provider, getProfileDetails) => async (accessToken, refreshToken, idToken, profile, cb) => {
|
||||||
try {
|
try {
|
||||||
const { email, id, avatarUrl, username, name, emailVerified } = getProfileDetails({
|
const { email, id, avatarUrl, username, name, emailVerified } = getProfileDetails({
|
||||||
idToken, profile,
|
idToken,
|
||||||
|
profile,
|
||||||
});
|
});
|
||||||
|
|
||||||
const oldUser = await findUser({ email: email.trim() });
|
const oldUser = await findUser({ email: email.trim() });
|
||||||
|
|
6
api/test/__mocks__/openid-client-passport.js
Normal file
6
api/test/__mocks__/openid-client-passport.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
// api/test/__mocks__/openid-client-passport.js
|
||||||
|
const Strategy = jest.fn().mockImplementation((options, verify) => {
|
||||||
|
return { name: 'mocked-openid-passport-strategy', options, verify };
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = { Strategy };
|
67
api/test/__mocks__/openid-client.js
Normal file
67
api/test/__mocks__/openid-client.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// api/test/__mocks__/openid-client.js
|
||||||
|
module.exports = {
|
||||||
|
Issuer: {
|
||||||
|
discover: jest.fn().mockResolvedValue({
|
||||||
|
Client: jest.fn().mockImplementation(() => ({
|
||||||
|
authorizationUrl: jest.fn().mockReturnValue('mock_auth_url'),
|
||||||
|
callback: jest.fn().mockResolvedValue({
|
||||||
|
access_token: 'mock_access_token',
|
||||||
|
id_token: 'mock_id_token',
|
||||||
|
claims: () => ({
|
||||||
|
sub: 'mock_sub',
|
||||||
|
email: 'mock@example.com',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
userinfo: jest.fn().mockResolvedValue({
|
||||||
|
sub: 'mock_sub',
|
||||||
|
email: 'mock@example.com',
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
Strategy: jest.fn().mockImplementation((options, verify) => {
|
||||||
|
// Store verify to call it if needed, or just mock the strategy behavior
|
||||||
|
return { name: 'openid-mock-strategy' };
|
||||||
|
}),
|
||||||
|
custom: {
|
||||||
|
setHttpOptionsDefaults: jest.fn(),
|
||||||
|
},
|
||||||
|
// Add any other exports from openid-client that are used directly
|
||||||
|
// For example, if your code uses `client.Issuer.discover`, then mock `Issuer`
|
||||||
|
// If it uses `new Strategy()`, then mock `Strategy`
|
||||||
|
// Based on openidStrategy.js, it uses:
|
||||||
|
// const client = require('openid-client'); -> client.discovery, client.fetchUserInfo, client.genericGrantRequest
|
||||||
|
// const { Strategy: OpenIDStrategy } = require('openid-client/passport');
|
||||||
|
// So the mock needs to cover these.
|
||||||
|
// The provided mock in openidStrategy.spec.js is a good reference.
|
||||||
|
|
||||||
|
// Simpler mock based on the spec file:
|
||||||
|
discovery: jest.fn().mockResolvedValue({
|
||||||
|
clientId: 'fake_client_id',
|
||||||
|
clientSecret: 'fake_client_secret',
|
||||||
|
issuer: 'https://fake-issuer.com',
|
||||||
|
Client: jest.fn().mockImplementation(() => ({
|
||||||
|
authorizationUrl: jest.fn().mockReturnValue('mock_auth_url'),
|
||||||
|
callback: jest.fn().mockResolvedValue({
|
||||||
|
access_token: 'mock_access_token',
|
||||||
|
id_token: 'mock_id_token',
|
||||||
|
claims: () => ({
|
||||||
|
sub: 'mock_sub',
|
||||||
|
email: 'mock@example.com',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
userinfo: jest.fn().mockResolvedValue({
|
||||||
|
sub: 'mock_sub',
|
||||||
|
email: 'mock@example.com',
|
||||||
|
}),
|
||||||
|
grant: jest.fn().mockResolvedValue({ access_token: 'mock_grant_token' }), // For genericGrantRequest
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
fetchUserInfo: jest.fn().mockResolvedValue({
|
||||||
|
preferred_username: 'preferred_username',
|
||||||
|
}),
|
||||||
|
genericGrantRequest: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ access_token: 'mock_grant_access_token', expires_in: 3600 }),
|
||||||
|
customFetch: Symbol('customFetch'),
|
||||||
|
};
|
178
package-lock.json
generated
178
package-lock.json
generated
|
@ -91,6 +91,7 @@
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"jwks-rsa": "^3.2.0",
|
||||||
"keyv": "^5.3.2",
|
"keyv": "^5.3.2",
|
||||||
"keyv-file": "^5.1.2",
|
"keyv-file": "^5.1.2",
|
||||||
"klona": "^2.0.6",
|
"klona": "^2.0.6",
|
||||||
|
@ -108,7 +109,7 @@
|
||||||
"ollama": "^0.5.0",
|
"ollama": "^0.5.0",
|
||||||
"openai": "^4.96.2",
|
"openai": "^4.96.2",
|
||||||
"openai-chat-tokens": "^0.2.8",
|
"openai-chat-tokens": "^0.2.8",
|
||||||
"openid-client": "^5.4.2",
|
"openid-client": "^6.5.0",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-apple": "^2.0.2",
|
"passport-apple": "^2.0.2",
|
||||||
"passport-discord": "^0.1.4",
|
"passport-discord": "^0.1.4",
|
||||||
|
@ -814,6 +815,15 @@
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"api/node_modules/jose": {
|
||||||
|
"version": "6.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz",
|
||||||
|
"integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"api/node_modules/keyv-file": {
|
"api/node_modules/keyv-file": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/keyv-file/-/keyv-file-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/keyv-file/-/keyv-file-5.1.2.tgz",
|
||||||
|
@ -1023,6 +1033,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"api/node_modules/openid-client": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-fAfYaTnOYE2kQCqEJGX9KDObW2aw7IQy4jWpU/+3D3WoCFLbix5Hg6qIPQ6Js9r7f8jDUmsnnguRNCSw4wU/IQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jose": "^6.0.10",
|
||||||
|
"oauth4webapi": "^3.5.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"api/node_modules/sharp": {
|
"api/node_modules/sharp": {
|
||||||
"version": "0.33.5",
|
"version": "0.33.5",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
||||||
|
@ -25002,7 +25025,6 @@
|
||||||
"version": "1.19.5",
|
"version": "1.19.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||||
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/connect": "*",
|
"@types/connect": "*",
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
|
@ -25012,7 +25034,6 @@
|
||||||
"version": "3.4.38",
|
"version": "3.4.38",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
|
@ -25101,8 +25122,7 @@
|
||||||
"node_modules/@types/http-errors": {
|
"node_modules/@types/http-errors": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
||||||
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
|
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/istanbul-lib-coverage": {
|
"node_modules/@types/istanbul-lib-coverage": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
|
@ -25172,6 +25192,16 @@
|
||||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/jsonwebtoken": {
|
||||||
|
"version": "9.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz",
|
||||||
|
"integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/ms": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/katex": {
|
"node_modules/@types/katex": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz",
|
||||||
|
@ -25203,8 +25233,7 @@
|
||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/ms": {
|
"node_modules/@types/ms": {
|
||||||
"version": "0.7.34",
|
"version": "0.7.34",
|
||||||
|
@ -25242,14 +25271,12 @@
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.9.17",
|
"version": "6.9.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
|
||||||
"integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==",
|
"integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/range-parser": {
|
"node_modules/@types/range-parser": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.2.53",
|
"version": "18.2.53",
|
||||||
|
@ -25300,7 +25327,6 @@
|
||||||
"version": "0.17.4",
|
"version": "0.17.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
|
||||||
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
|
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/mime": "^1",
|
"@types/mime": "^1",
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
|
@ -25310,7 +25336,6 @@
|
||||||
"version": "1.15.7",
|
"version": "1.15.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
|
||||||
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
|
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/http-errors": "*",
|
"@types/http-errors": "*",
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
|
@ -33822,6 +33847,47 @@
|
||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jwks-rsa": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "^4.17.20",
|
||||||
|
"@types/jsonwebtoken": "^9.0.4",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"jose": "^4.15.4",
|
||||||
|
"limiter": "^1.1.5",
|
||||||
|
"lru-memoizer": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jwks-rsa/node_modules/@types/express": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/body-parser": "*",
|
||||||
|
"@types/express-serve-static-core": "^4.17.33",
|
||||||
|
"@types/qs": "*",
|
||||||
|
"@types/serve-static": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jwks-rsa/node_modules/@types/express-serve-static-core": {
|
||||||
|
"version": "4.19.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
|
||||||
|
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"@types/qs": "*",
|
||||||
|
"@types/range-parser": "*",
|
||||||
|
"@types/send": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jws": {
|
"node_modules/jws": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||||
|
@ -34142,6 +34208,11 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/limiter": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
|
||||||
|
},
|
||||||
"node_modules/lines-and-columns": {
|
"node_modules/lines-and-columns": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||||
|
@ -34483,6 +34554,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
|
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.clonedeep": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.debounce": {
|
"node_modules/lodash.debounce": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
|
@ -34860,6 +34937,34 @@
|
||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lru-memoizer": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lru-cache": "6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lru-memoizer/node_modules/lru-cache": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lru-memoizer/node_modules/yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/lucide-react": {
|
"node_modules/lucide-react": {
|
||||||
"version": "0.394.0",
|
"version": "0.394.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.394.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.394.0.tgz",
|
||||||
|
@ -36779,6 +36884,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz",
|
||||||
"integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q=="
|
"integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q=="
|
||||||
},
|
},
|
||||||
|
"node_modules/oauth4webapi": {
|
||||||
|
"version": "3.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.5.1.tgz",
|
||||||
|
"integrity": "sha512-txg/jZQwcbaF7PMJgY7aoxc9QuCxHVFMiEkDIJ60DwDz3PbtXPQnrzo+3X4IRYGChIwWLabRBRpf1k9hO9+xrQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
@ -36920,14 +37034,6 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/oidc-token-hash": {
|
|
||||||
"version": "5.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
|
|
||||||
"integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==",
|
|
||||||
"engines": {
|
|
||||||
"node": "^10.13.0 || >=12.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ollama": {
|
"node_modules/ollama": {
|
||||||
"version": "0.5.14",
|
"version": "0.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.14.tgz",
|
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.14.tgz",
|
||||||
|
@ -37055,36 +37161,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||||
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="
|
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="
|
||||||
},
|
},
|
||||||
"node_modules/openid-client": {
|
|
||||||
"version": "5.6.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.4.tgz",
|
|
||||||
"integrity": "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==",
|
|
||||||
"dependencies": {
|
|
||||||
"jose": "^4.15.4",
|
|
||||||
"lru-cache": "^6.0.0",
|
|
||||||
"object-hash": "^2.2.0",
|
|
||||||
"oidc-token-hash": "^5.0.3"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/panva"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/openid-client/node_modules/lru-cache": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
|
||||||
"dependencies": {
|
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/openid-client/node_modules/yallist": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
|
||||||
},
|
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.3",
|
"version": "0.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
||||||
|
|
|
@ -1056,6 +1056,10 @@ export enum CacheKeys {
|
||||||
* Key for s3 check intervals per user
|
* Key for s3 check intervals per user
|
||||||
*/
|
*/
|
||||||
S3_EXPIRY_INTERVAL = 'S3_EXPIRY_INTERVAL',
|
S3_EXPIRY_INTERVAL = 'S3_EXPIRY_INTERVAL',
|
||||||
|
/**
|
||||||
|
* key for open id exchanged tokens
|
||||||
|
*/
|
||||||
|
OPENID_EXCHANGED_TOKENS = 'OPENID_EXCHANGED_TOKENS',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue