made proggress on oidc with better try catch

pull/3743/head
Mike Kao 1 year ago
parent 55cea08e85
commit d00cf64501

@ -6,7 +6,7 @@ import { Permission } from '@server/lib/permissions';
import { getSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings';
import logger from '@server/logger'; import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth'; import { isAuthenticated } from '@server/middleware/auth';
import { Router } from 'express'; import { Router, Request } from 'express';
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { import {
createJwtSchema, createJwtSchema,
@ -443,160 +443,185 @@ authRoutes.get('/oidc-login', async (req, res, next) => {
authRoutes.get('/oidc-callback', async (req, res, next) => { authRoutes.get('/oidc-callback', async (req, res, next) => {
logger.info('OIDC callback initiated', { req });
const settings = getSettings();
const { oidcDomain, oidcClientId, oidcClientSecret } = settings.main;
if (!settings.main.oidcLogin) {
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 scope = url.searchParams.get('scope'); // Optional scope parameter
// Optional logging for scope parameter
if (scope) {
logger.info('OIDC callback with scope', { scope });
} else {
logger.info('OIDC callback without scope');
}
try { try {
// Check that the request belongs to the correct state const logRequestInfo = (req: Request) => {
if (state && cookieState === state) { const remoteIp = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
res.clearCookie('oidc-state'); const requestInfo = {
} else { method: req.method,
logger.info('Failed OIDC login attempt', { url: req.url,
cause: 'Invalid state', headers: req.headers,
ip: req.ip, remoteIp: remoteIp,
state: state, };
cookieState: cookieState,
}); return requestInfo;
return res.redirect('/login'); };
logger.info('OIDC callback initiated', { req: logRequestInfo(req) });
const settings = getSettings();
const { oidcDomain, oidcClientId, oidcClientSecret } = settings.main;
if (!settings.main.oidcLogin) {
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 scope = url.searchParams.get('scope'); // Optional scope parameter
// Check that a code as been issued // Optional logging for scope parameter
const code = url.searchParams.get('code'); if (scope) {
if (!code) { logger.info('OIDC callback with scope', { scope });
logger.info('Failed OIDC login attempt', { } else {
cause: 'Invalid code', logger.info('OIDC callback without scope');
ip: req.ip,
code: code,
});
return res.redirect('/login');
} }
const wellKnownInfo = await getOIDCWellknownConfiguration(oidcDomain); try {
// Check that the request belongs to the correct state
if (state && cookieState === state) {
res.clearCookie('oidc-state');
} else {
logger.info('Failed OIDC login attempt', {
cause: 'Invalid state',
ip: req.ip,
state: state,
cookieState: cookieState,
});
return res.redirect('/login');
}
// Fetch the token data // Check that a code as been issued
const callbackUrl = new URL( const code = url.searchParams.get('code');
'/api/v1/auth/oidc-callback', if (!code) {
`${req.protocol}://${req.headers.host}` logger.info('Failed OIDC login attempt', {
); cause: 'Invalid code',
ip: req.ip,
code: code,
});
return res.redirect('/login');
}
const formData = new URLSearchParams(); const wellKnownInfo = await getOIDCWellknownConfiguration(oidcDomain);
formData.append('client_secret', oidcClientSecret);
formData.append('grant_type', 'authorization_code');
formData.append('redirect_uri', callbackUrl.toString());
formData.append('client_id', oidcClientId);
formData.append('code', code);
// Append scope if available
if (scope) {
formData.append('scope', scope);
}
const response = await fetch(wellKnownInfo.token_endpoint, { // Fetch the token data
method: 'POST', const callbackUrl = new URL(
headers: new Headers([ '/api/v1/auth/oidc-callback',
['Content-Type', 'application/x-www-form-urlencoded'], `${req.protocol}://${req.headers.host}`
]), );
body: formData,
});
// Check that the response is valid const formData = new URLSearchParams();
const body = (await response.json()) as formData.append('client_secret', oidcClientSecret);
| { id_token: string; error: never } formData.append('grant_type', 'authorization_code');
| { error: string }; formData.append('redirect_uri', callbackUrl.toString());
if (body.error) { formData.append('client_id', oidcClientId);
logger.info('Failed OIDC login attempt', { formData.append('code', code);
cause: 'Invalid token response', // Append scope if available
ip: req.ip, if (scope) {
body: body, formData.append('scope', scope);
}); }
return res.redirect('/login');
}
// Validate that the token response is valid and not manipulated const response = await fetch(wellKnownInfo.token_endpoint, {
const { id_token: idToken } = body as Extract< method: 'POST',
typeof body, headers: new Headers([
{ id_token: string } ['Content-Type', 'application/x-www-form-urlencoded'],
>; ]),
try { body: formData,
const decoded = decodeJwt(idToken);
const jwtSchema = createJwtSchema({
oidcClientId: oidcClientId,
oidcDomain: oidcDomain,
}); });
await jwtSchema.validate(decoded); // Check that the response is valid
} catch { const body = (await response.json()) as
logger.info('Failed OIDC login attempt', { | { id_token: string; error: never }
cause: 'Invalid jwt', | { error: string };
ip: req.ip, if (body.error) {
idToken: idToken, logger.info('Failed OIDC login attempt', {
}); cause: 'Invalid token response',
return res.redirect('/login'); ip: req.ip,
} body: body,
});
return res.redirect('/login');
}
// Check that email is verified and map email to user // Validate that the token response is valid and not manipulated
const decoded: InferType<ReturnType<typeof createJwtSchema>> = const { id_token: idToken } = body as Extract<
decodeJwt(idToken); typeof body,
{ id_token: string }
>;
try {
const decoded = decodeJwt(idToken);
const jwtSchema = createJwtSchema({
oidcClientId: oidcClientId,
oidcDomain: oidcDomain,
});
if (!decoded.email_verified) { await jwtSchema.validate(decoded);
logger.info('Failed OIDC login attempt', { } catch {
cause: 'Email not verified', logger.info('Failed OIDC login attempt', {
ip: req.ip, cause: 'Invalid jwt',
email: decoded.email, ip: req.ip,
idToken: idToken,
});
return res.redirect('/login');
}
// Check that email is verified and map email to user
const decoded: InferType<ReturnType<typeof createJwtSchema>> =
decodeJwt(idToken);
if (!decoded.email_verified) {
logger.info('Failed OIDC login attempt', {
cause: 'Email not verified',
ip: req.ip,
email: decoded.email,
});
return res.redirect('/login');
}
const userRepository = getRepository(User);
let user = await userRepository.findOne({
where: { email: decoded.email },
}); });
return res.redirect('/login');
}
const userRepository = getRepository(User); // Create user if it doesn't exist
let user = await userRepository.findOne({ if (!user) {
where: { email: decoded.email }, logger.info(`Creating user for ${decoded.email}`, {
}); 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,
});
await userRepository.save(user);
}
// Create user if it doesn't exist // Set logged in session and return
if (!user) { if (req.session) {
logger.info(`Creating user for ${decoded.email}`, { req.session.userId = user.id;
}
return res.redirect('/');
} catch (error) {
logger.error('Failed OIDC login attempt', {
cause: 'Unknown error',
ip: req.ip, ip: req.ip,
email: decoded.email, errorMessage: error.message,
});
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,
}); });
await userRepository.save(user); return res.redirect('/login');
}
// Set logged in session and return
if (req.session) {
req.session.userId = user.id;
} }
return res.redirect('/');
} catch (error) { } catch (error) {
logger.error('Failed OIDC login attempt', { // Log the error details
cause: 'Unknown error', logger.error('Error in OIDC callback', {
ip: req.ip, path: '/oidc-callback',
errorMessage: error.message, error: error.message,
stack: error.stack, // Include the error stack trace for debugging
}); });
return res.redirect('/login');
// Handle the error as appropriate for your application
next(error);
} }
}); });

Loading…
Cancel
Save