*️⃣ 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:
Peter 2025-05-22 14:19:24 +02:00 committed by Danny Avila
parent d47d827ed9
commit bf80cf30b3
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
24 changed files with 690 additions and 191 deletions

View file

@ -4,9 +4,10 @@ const googleLogin = require('./googleStrategy');
const githubLogin = require('./githubStrategy');
const discordLogin = require('./discordStrategy');
const facebookLogin = require('./facebookStrategy');
const setupOpenId = require('./openidStrategy');
const { setupOpenId, getOpenIdConfig } = require('./openidStrategy');
const jwtLogin = require('./jwtStrategy');
const ldapLogin = require('./ldapStrategy');
const openIdJwtLogin = require('./openIdJwtStrategy');
module.exports = {
appleLogin,
@ -17,5 +18,7 @@ module.exports = {
jwtLogin,
facebookLogin,
setupOpenId,
getOpenIdConfig,
ldapLogin,
};
openIdJwtLogin,
};

View file

@ -4,7 +4,7 @@ const { getUserById, updateUser } = require('~/models');
const { logger } = require('~/config');
// JWT strategy
const jwtLogin = async () =>
const jwtLogin = () =>
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),

View 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;

View file

@ -1,28 +1,101 @@
const { CacheKeys } = require('librechat-data-provider');
const fetch = require('node-fetch');
const passport = require('passport');
const jwtDecode = require('jsonwebtoken/decode');
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 { findUser, createUser, updateUser } = require('~/models/userMethods');
const { hashToken } = require('~/server/utils/crypto');
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
const getLogStores = require('~/cache/getLogStores');
let crypto;
try {
crypto = require('node:crypto');
} catch (err) {
logger.error('[openidStrategy] crypto support is disabled!', err);
/**
* @typedef {import('openid-client').ClientMetadata} ClientMetadata
* @typedef {import('openid-client').Configuration} Configuration
**/
/** @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.
* @param {string} url
* @param {string} accessToken
* @returns {Promise<Buffer>}
* @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<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) {
return '';
}
@ -31,7 +104,7 @@ const downloadImage = async (url, accessToken) => {
const options = {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Authorization: `Bearer ${exchangedAccessToken}`,
},
};
@ -105,63 +178,68 @@ function convertToUsername(input, 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() {
try {
if (process.env.PROXY) {
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} */
/** @type {ClientMetadata} */
const clientMetadata = {
client_id: process.env.OPENID_CLIENT_ID,
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 =
issuer.id_token_signing_alg_values_supported?.[0] || 'RS256';
/** @type {Configuration} */
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 requiredRoleParameterPath = process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH;
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,
params: {
scope: process.env.OPENID_SCOPE,
},
config: openidConfig,
scope: process.env.OPENID_SCOPE,
callbackURL: process.env.DOMAIN_SERVER + process.env.OPENID_CALLBACK_URL,
usePKCE,
},
async (tokenset, userinfo, done) => {
async (tokenset, done) => {
try {
logger.info(`[openidStrategy] verify login openidId: ${userinfo.sub}`);
logger.debug('[openidStrategy] very login tokenset and userinfo', { tokenset, userinfo });
let user = await findUser({ openidId: userinfo.sub });
const claims = tokenset.claims();
let user = await findUser({ openidId: claims.sub });
logger.info(
`[openidStrategy] user ${user ? 'found' : 'not found'} with openidId: ${userinfo.sub}`,
`[openidStrategy] user ${user ? 'found' : 'not found'} with openidId: ${claims.sub}`,
);
if (!user) {
user = await findUser({ email: userinfo.email });
user = await findUser({ email: claims.email });
logger.info(
`[openidStrategy] user ${user ? 'found' : 'not found'} with email: ${
userinfo.email
} for openidId: ${userinfo.sub}`,
claims.email
} for openidId: ${claims.sub}`,
);
}
const userinfo = {
...claims,
...(await getUserInfo(openidConfig, tokenset.access_token, claims.sub)),
};
const fullName = getFullName(userinfo);
if (requiredRole) {
@ -220,7 +298,7 @@ async function setupOpenId() {
user.name = fullName;
}
if (userinfo.picture && !user.avatar?.includes('manual=true')) {
if (!!userinfo && userinfo.picture && !user.avatar?.includes('manual=true')) {
/** @type {string | undefined} */
const imageUrl = userinfo.picture;
@ -231,7 +309,12 @@ async function setupOpenId() {
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) {
const { saveBuffer } = getStrategyFunctions(process.env.CDN_PROVIDER);
const imagePath = await saveBuffer({
@ -257,18 +340,34 @@ async function setupOpenId() {
},
);
done(null, user);
done(null, { ...user, tokenset });
} catch (err) {
logger.error('[openidStrategy] login failed', err);
done(err);
}
},
);
passport.use('openid', openidLogin);
return openidConfig;
} catch (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,
};

View file

@ -1,16 +1,13 @@
const fetch = require('node-fetch');
const jwtDecode = require('jsonwebtoken/decode');
const { Issuer, Strategy: OpenIDStrategy } = require('openid-client');
const { findUser, createUser, updateUser } = require('~/models/userMethods');
const setupOpenId = require('./openidStrategy');
const { setupOpenId } = require('./openidStrategy');
// --- Mocks ---
jest.mock('node-fetch');
jest.mock('openid-client');
jest.mock('jsonwebtoken/decode');
jest.mock('~/server/services/Files/strategies', () => ({
getStrategyFunctions: jest.fn(() => ({
// You can modify this mock as needed (here returning a dummy function)
saveBuffer: jest.fn().mockResolvedValue('/fake/path/to/avatar.png'),
})),
}));
@ -23,38 +20,73 @@ jest.mock('~/server/utils/crypto', () => ({
hashToken: jest.fn().mockResolvedValue('hashed-token'),
}));
jest.mock('~/server/utils', () => ({
isEnabled: jest.fn(() => false), // default to false, override per test if needed
isEnabled: jest.fn(() => false),
}));
jest.mock('~/config', () => ({
logger: {
info: jest.fn(),
debug: 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
Issuer.discover = jest.fn().mockResolvedValue({
id_token_signing_alg_values_supported: ['RS256'],
Client: jest.fn().mockImplementation((clientMetadata) => {
return {
metadata: clientMetadata,
};
}),
// Mock the openid-client module and all its dependencies
jest.mock('openid-client', () => {
return {
discovery: jest.fn().mockResolvedValue({
clientId: 'fake_client_id',
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
let verifyCallback;
OpenIDStrategy.mockImplementation((options, verify) => {
verifyCallback = verify;
return { name: 'openid', options, verify };
jest.mock('openid-client/passport', () => {
let verifyCallback;
const mockStrategy = jest.fn((options, verify) => {
verifyCallback = verify;
return { name: 'openid', options, verify };
});
return {
Strategy: mockStrategy,
__getVerifyCallback: () => verifyCallback,
};
});
// Mock passport
jest.mock('passport', () => ({
use: jest.fn(),
}));
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
const validate = (tokenset, userinfo) =>
const validate = (tokenset) =>
new Promise((resolve, reject) => {
verifyCallback(tokenset, userinfo, (err, user, details) => {
verifyCallback(tokenset, (err, user, details) => {
if (err) {
reject(err);
} else {
@ -66,17 +98,16 @@ describe('setupOpenId', () => {
const tokenset = {
id_token: 'fake_id_token',
access_token: 'fake_access_token',
};
const baseUserinfo = {
sub: '1234',
email: 'test@example.com',
email_verified: true,
given_name: 'First',
family_name: 'Last',
name: 'My Full',
username: 'flast',
picture: 'https://example.com/avatar.png',
claims: () => ({
sub: '1234',
email: 'test@example.com',
email_verified: true,
given_name: 'First',
family_name: 'Last',
name: 'My Full',
username: 'flast',
picture: 'https://example.com/avatar.png',
}),
};
beforeEach(async () => {
@ -96,6 +127,7 @@ describe('setupOpenId', () => {
delete process.env.OPENID_USERNAME_CLAIM;
delete process.env.OPENID_NAME_CLAIM;
delete process.env.PROXY;
delete process.env.OPENID_USE_PKCE;
// Default jwtDecode mock returns a token that includes the required role.
jwtDecode.mockReturnValue({
@ -120,16 +152,17 @@ describe('setupOpenId', () => {
};
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();
verifyCallback = require('openid-client/passport').__getVerifyCallback();
});
it('should create a new user with correct username when username claim exists', async () => {
// Arrange our userinfo already has username 'flast'
const userinfo = { ...baseUserinfo };
const userinfo = tokenset.claims();
// Act
const { user } = await validate(tokenset, userinfo);
const { user } = await validate(tokenset);
// Assert
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 () => {
// Arrange remove username from userinfo
const userinfo = { ...baseUserinfo };
const userinfo = { ...tokenset.claims() };
delete userinfo.username;
// Expect the username to be the given name (unchanged case)
const expectUsername = userinfo.given_name;
// Act
const { user } = await validate(tokenset, userinfo);
const { user } = await validate({ ...tokenset, claims: () => userinfo });
// Assert
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 () => {
// Arrange remove username and given_name
const userinfo = { ...baseUserinfo };
const userinfo = { ...tokenset.claims() };
delete userinfo.username;
delete userinfo.given_name;
const expectUsername = userinfo.email;
// Act
const { user } = await validate(tokenset, userinfo);
const { user } = await validate({ ...tokenset, claims: () => userinfo });
// Assert
expect(user.username).toBe(expectUsername);
@ -187,10 +220,10 @@ describe('setupOpenId', () => {
it('should override username with OPENID_USERNAME_CLAIM when set', async () => {
// Arrange set OPENID_USERNAME_CLAIM so that the sub claim is used
process.env.OPENID_USERNAME_CLAIM = 'sub';
const userinfo = { ...baseUserinfo };
const userinfo = tokenset.claims();
// Act
const { user } = await validate(tokenset, userinfo);
const { user } = await validate(tokenset);
// Assert username should equal the sub (converted as-is)
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 () => {
// Arrange
const userinfo = { ...baseUserinfo };
const userinfo = tokenset.claims();
const expectedFullName = `${userinfo.given_name} ${userinfo.family_name}`;
// Act
const { user } = await validate(tokenset, userinfo);
const { user } = await validate(tokenset);
// Assert
expect(user.name).toBe(expectedFullName);
@ -216,10 +249,10 @@ describe('setupOpenId', () => {
it('should override full name with OPENID_NAME_CLAIM when set', async () => {
// Arrange use the name claim as the full name
process.env.OPENID_NAME_CLAIM = 'name';
const userinfo = { ...baseUserinfo, name: 'Custom Name' };
const userinfo = { ...tokenset.claims(), name: 'Custom Name' };
// Act
const { user } = await validate(tokenset, userinfo);
const { user } = await validate({ ...tokenset, claims: () => userinfo });
// Assert
expect(user.name).toBe('Custom Name');
@ -230,31 +263,31 @@ describe('setupOpenId', () => {
const existingUser = {
_id: 'existingUserId',
provider: 'local',
email: baseUserinfo.email,
email: tokenset.claims().email,
openidId: '',
username: '',
name: '',
};
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 null;
});
const userinfo = { ...baseUserinfo };
const userinfo = tokenset.claims();
// Act
await validate(tokenset, userinfo);
await validate(tokenset);
// Assert updateUser should be called and the user object updated
expect(updateUser).toHaveBeenCalledWith(
existingUser._id,
expect.objectContaining({
provider: 'openid',
openidId: baseUserinfo.sub,
username: baseUserinfo.username,
name: `${baseUserinfo.given_name} ${baseUserinfo.family_name}`,
openidId: userinfo.sub,
username: userinfo.username,
name: `${userinfo.given_name} ${userinfo.family_name}`,
}),
);
});
@ -264,10 +297,10 @@ describe('setupOpenId', () => {
jwtDecode.mockReturnValue({
roles: ['SomeOtherRole'],
});
const userinfo = { ...baseUserinfo };
const userinfo = tokenset.claims();
// Act
const { user, details } = await validate(tokenset, userinfo);
const { user, details } = await validate(tokenset);
// Assert verify that the strategy rejects login
expect(user).toBe(false);
@ -276,10 +309,10 @@ describe('setupOpenId', () => {
it('should attempt to download and save the avatar if picture is provided', async () => {
// Arrange ensure userinfo contains a picture URL
const userinfo = { ...baseUserinfo };
const userinfo = tokenset.claims();
// 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
expect(fetch).toHaveBeenCalled();
@ -289,14 +322,25 @@ describe('setupOpenId', () => {
it('should not attempt to download avatar if picture is not provided', async () => {
// Arrange remove picture
const userinfo = { ...baseUserinfo };
const userinfo = { ...tokenset.claims() };
delete userinfo.picture;
// Act
await validate(tokenset, userinfo);
await validate({ ...tokenset, claims: () => userinfo });
// Assert fetch should not be called and avatar should remain undefined or empty
expect(fetch).not.toHaveBeenCalled();
// 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();
});
});

View file

@ -7,7 +7,8 @@ const socialLogin =
(provider, getProfileDetails) => async (accessToken, refreshToken, idToken, profile, cb) => {
try {
const { email, id, avatarUrl, username, name, emailVerified } = getProfileDetails({
idToken, profile,
idToken,
profile,
});
const oldUser = await findUser({ email: email.trim() });