|
|
|
@ -443,52 +443,48 @@ authRoutes.get('/oidc-login', async (req, res, next) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
authRoutes.get('/oidc-callback', async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const settings = getSettings();
|
|
|
|
|
const { oidcDomain, oidcClientId, oidcClientSecret } = settings.main;
|
|
|
|
|
|
|
|
|
|
// Log the initial OIDC callback request
|
|
|
|
|
logger.info('Received OIDC callback', { ip: req.ip });
|
|
|
|
|
|
|
|
|
|
if (!settings.main.oidcLogin) {
|
|
|
|
|
logger.warn('OIDC sign-in is disabled', { path: '/oidc-callback' });
|
|
|
|
|
logger.warn('OIDC sign-in is disabled', { ip: req.ip });
|
|
|
|
|
return res.status(500).json({ error: 'OIDC sign-in is disabled.' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cookieState = req.cookies['oidc-state'];
|
|
|
|
|
const url = new URL(req.url, `${req.protocol}://${req.hostname}`);
|
|
|
|
|
const state = url.searchParams.get('state');
|
|
|
|
|
const code = url.searchParams.get('code');
|
|
|
|
|
const scope = url.searchParams.get('scope'); // Handling additional 'scope' parameter
|
|
|
|
|
|
|
|
|
|
logger.debug('OIDC callback received', {
|
|
|
|
|
path: '/oidc-callback',
|
|
|
|
|
state: state,
|
|
|
|
|
code: code,
|
|
|
|
|
cookieState: cookieState,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!state || state !== cookieState || !code) {
|
|
|
|
|
logger.warn('OIDC callback state or code mismatch or missing', {
|
|
|
|
|
path: '/oidc-callback',
|
|
|
|
|
state: state,
|
|
|
|
|
code: code,
|
|
|
|
|
cookieState: cookieState,
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
// State validation
|
|
|
|
|
if (!state || cookieState !== state) {
|
|
|
|
|
logger.warn('OIDC state mismatch', { ip: req.ip, state, cookieState });
|
|
|
|
|
return res.redirect('/login');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.clearCookie('oidc-state');
|
|
|
|
|
|
|
|
|
|
const wellKnownInfo = await getOIDCWellknownConfiguration(oidcDomain);
|
|
|
|
|
// Code validation
|
|
|
|
|
const code = url.searchParams.get('code');
|
|
|
|
|
if (!code) {
|
|
|
|
|
logger.warn('OIDC code missing', { ip: req.ip });
|
|
|
|
|
return res.redirect('/login');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const callbackUrl = new URL(
|
|
|
|
|
'/api/v1/auth/oidc-callback',
|
|
|
|
|
`${req.protocol}://${req.headers.host}`
|
|
|
|
|
).toString();
|
|
|
|
|
const wellKnownInfo = await getOIDCWellknownConfiguration(oidcDomain);
|
|
|
|
|
|
|
|
|
|
// Token request
|
|
|
|
|
const callbackUrl = new URL('/api/v1/auth/oidc-callback', `${req.protocol}://${req.headers.host}`);
|
|
|
|
|
const formData = new URLSearchParams();
|
|
|
|
|
formData.append('client_secret', oidcClientSecret);
|
|
|
|
|
formData.append('grant_type', 'authorization_code');
|
|
|
|
|
formData.append('redirect_uri', callbackUrl);
|
|
|
|
|
formData.append('redirect_uri', callbackUrl.toString());
|
|
|
|
|
formData.append('client_id', oidcClientId);
|
|
|
|
|
formData.append('code', code);
|
|
|
|
|
formData.append('scope', scope); // Include the 'scope' in the token request
|
|
|
|
|
|
|
|
|
|
const response = await fetch(wellKnownInfo.token_endpoint, {
|
|
|
|
|
method: 'POST',
|
|
|
|
@ -498,78 +494,53 @@ authRoutes.get('/oidc-callback', async (req, res, next) => {
|
|
|
|
|
body: formData,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Validate response
|
|
|
|
|
const body = await response.json();
|
|
|
|
|
|
|
|
|
|
if (body.error) {
|
|
|
|
|
logger.warn('Failed OIDC token exchange', {
|
|
|
|
|
path: '/oidc-callback',
|
|
|
|
|
error: body.error,
|
|
|
|
|
state: state,
|
|
|
|
|
code: code,
|
|
|
|
|
});
|
|
|
|
|
logger.warn('Invalid token response', { ip: req.ip, error: body.error });
|
|
|
|
|
return res.redirect('/login');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Token validation
|
|
|
|
|
const { id_token: idToken } = body;
|
|
|
|
|
const decoded = decodeJwt(idToken);
|
|
|
|
|
const jwtSchema = createJwtSchema({
|
|
|
|
|
oidcClientId: oidcClientId,
|
|
|
|
|
oidcDomain: oidcDomain,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let decoded;
|
|
|
|
|
try {
|
|
|
|
|
decoded = decodeJwt(idToken);
|
|
|
|
|
const jwtSchema = createJwtSchema({ oidcClientId, oidcDomain });
|
|
|
|
|
await jwtSchema.validate(decoded);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.warn('Invalid JWT in OIDC callback', {
|
|
|
|
|
path: '/oidc-callback',
|
|
|
|
|
error: error.message,
|
|
|
|
|
idToken: idToken,
|
|
|
|
|
});
|
|
|
|
|
logger.warn('JWT validation failed', { ip: req.ip, error: error.message });
|
|
|
|
|
return res.redirect('/login');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Email verification
|
|
|
|
|
if (!decoded.email_verified) {
|
|
|
|
|
logger.warn('Email not verified in OIDC callback', {
|
|
|
|
|
path: '/oidc-callback',
|
|
|
|
|
email: decoded.email,
|
|
|
|
|
});
|
|
|
|
|
logger.warn('Email not verified', { ip: req.ip, email: decoded.email });
|
|
|
|
|
return res.redirect('/login');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// User handling
|
|
|
|
|
const userRepository = getRepository(User);
|
|
|
|
|
let user = await userRepository.findOne({
|
|
|
|
|
where: { email: decoded.email },
|
|
|
|
|
});
|
|
|
|
|
let user = await userRepository.findOne({ where: { email: decoded.email } });
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
logger.info(`Creating new user from OIDC callback for ${decoded.email}`, {
|
|
|
|
|
path: '/oidc-callback',
|
|
|
|
|
email: decoded.email,
|
|
|
|
|
});
|
|
|
|
|
logger.info('Creating new user', { ip: req.ip, email: decoded.email });
|
|
|
|
|
const avatar = gravatarUrl(decoded.email, { default: 'mm', size: 200 });
|
|
|
|
|
user = new User({
|
|
|
|
|
avatar: avatar,
|
|
|
|
|
username: decoded.email,
|
|
|
|
|
email: decoded.email,
|
|
|
|
|
permissions: settings.main.defaultPermissions,
|
|
|
|
|
plexToken: '',
|
|
|
|
|
userType: UserType.LOCAL,
|
|
|
|
|
});
|
|
|
|
|
user = new User({ avatar, username: decoded.email, email: decoded.email, permissions: settings.main.defaultPermissions, plexToken: '', userType: UserType.LOCAL });
|
|
|
|
|
await userRepository.save(user);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Session handling
|
|
|
|
|
if (req.session) {
|
|
|
|
|
req.session.userId = user.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info('User logged in successfully', { ip: req.ip, userId: user.id });
|
|
|
|
|
return res.redirect('/');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Error in OIDC callback processing', {
|
|
|
|
|
path: '/oidc-callback',
|
|
|
|
|
error: error.message,
|
|
|
|
|
});
|
|
|
|
|
next(error);
|
|
|
|
|
logger.error('OIDC login error', { ip: req.ip, errorMessage: error.message });
|
|
|
|
|
return res.redirect('/login');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default authRoutes;
|
|
|
|
|