got oidc working for authelia

pull/3743/head
Mike Kao 5 months ago
parent d00cf64501
commit 4c87717d65

@ -12,6 +12,7 @@ import {
createJwtSchema,
getOIDCRedirectUrl,
getOIDCWellknownConfiguration,
OIDCJwtPayload,
} from '@server/utils/oidc';
import { randomBytes } from 'crypto';
import gravatarUrl from 'gravatar-url';
@ -452,7 +453,6 @@ authRoutes.get('/oidc-callback', async (req, res, next) => {
headers: req.headers,
remoteIp: remoteIp,
};
return requestInfo;
};
@ -464,154 +464,132 @@ authRoutes.get('/oidc-callback', async (req, res, next) => {
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 {
// 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');
}
// Check that a code as been issued
const code = url.searchParams.get('code');
if (!code) {
logger.info('Failed OIDC login attempt', {
cause: 'Invalid code',
ip: req.ip,
code: code,
});
return res.redirect('/login');
}
// 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');
}
const wellKnownInfo = await getOIDCWellknownConfiguration(oidcDomain);
const code = url.searchParams.get('code');
if (!code) {
logger.info('Failed OIDC login attempt', {
cause: 'Invalid code',
ip: req.ip,
code: code,
});
return res.redirect('/login');
}
// Fetch the token data
const callbackUrl = new URL(
'/api/v1/auth/oidc-callback',
`${req.protocol}://${req.headers.host}`
);
const wellKnownInfo = await getOIDCWellknownConfiguration(oidcDomain);
const formData = new URLSearchParams();
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 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.toString());
formData.append('client_id', oidcClientId);
formData.append('code', code);
if (scope) {
formData.append('scope', scope);
}
const response = await fetch(wellKnownInfo.token_endpoint, {
method: 'POST',
headers: new Headers([
['Content-Type', 'application/x-www-form-urlencoded'],
]),
body: formData,
});
const response = await fetch(wellKnownInfo.token_endpoint, {
method: 'POST',
headers: new Headers([['Content-Type', 'application/x-www-form-urlencoded']]),
body: formData,
});
// Check that the response is valid
const body = (await response.json()) as
| { id_token: string; error: never }
| { error: string };
if (body.error) {
logger.info('Failed OIDC login attempt', {
cause: 'Invalid token response',
ip: req.ip,
body: body,
});
return res.redirect('/login');
}
const body = (await response.json()) as
| { id_token: string; error: never }
| { error: string };
// Validate that the token response is valid and not manipulated
const { id_token: idToken } = body as Extract<
typeof body,
{ id_token: string }
>;
try {
const decoded = decodeJwt(idToken);
const jwtSchema = createJwtSchema({
oidcClientId: oidcClientId,
oidcDomain: oidcDomain,
});
if (body.error) {
logger.info('Failed OIDC login attempt', {
cause: `Invalid token response: ${body.error}`,
ip: req.ip,
body: body,
});
return res.redirect('/login');
}
await jwtSchema.validate(decoded);
} catch {
logger.info('Failed OIDC login attempt', {
cause: 'Invalid jwt',
ip: req.ip,
idToken: idToken,
});
return res.redirect('/login');
}
const { id_token: idToken } = body as Extract<typeof body, { id_token: string }>;
try {
const decoded = decodeJwt<OIDCJwtPayload>(idToken);
const jwtSchema = createJwtSchema({
oidcClientId: oidcClientId,
oidcDomain: oidcDomain,
});
// Check that email is verified and map email to user
const decoded: InferType<ReturnType<typeof createJwtSchema>> =
decodeJwt(idToken);
await jwtSchema.validate(decoded);
} catch (error) {
logger.info('Failed OIDC login attempt', {
cause: `Invalid JWT: ${error.message}`,
ip: req.ip,
idToken: idToken,
});
return res.redirect('/login');
}
if (!decoded.email_verified) {
logger.info('Failed OIDC login attempt', {
cause: 'Email not verified',
ip: req.ip,
email: decoded.email,
});
return res.redirect('/login');
}
// Check that email is verified and map email to user
const decoded: InferType<ReturnType<typeof createJwtSchema>> =
decodeJwt(idToken);
const userRepository = getRepository(User);
let user = await userRepository.findOne({
where: { email: decoded.email },
if (!decoded.email_verified) {
logger.info('Failed OIDC login attempt', {
cause: 'Email not verified',
ip: req.ip,
email: decoded.email,
});
return res.redirect('/login');
}
// Create user if it doesn't exist
if (!user) {
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);
}
const userRepository = getRepository(User);
let user = await userRepository.findOne({
where: { email: decoded.email },
});
// Set logged in session and return
if (req.session) {
req.session.userId = user.id;
}
return res.redirect('/');
} catch (error) {
logger.error('Failed OIDC login attempt', {
cause: 'Unknown error',
// Create user if it doesn't exist
if (!user) {
logger.info(`Creating user for ${decoded.email}`, {
ip: req.ip,
errorMessage: error.message,
email: decoded.email,
});
return res.redirect('/login');
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);
}
// Set logged in session and return
if (req.session) {
req.session.userId = user.id;
}
return res.redirect('/');
} catch (error) {
// Log the error details
logger.error('Error in OIDC callback', {

@ -50,13 +50,19 @@ export const createJwtSchema = ({
`The token iss value doesn't match the oidc_DOMAIN (${oidcDomain})`
)
.required("The token didn't come with an iss value."),
aud: yup
.string()
.oneOf(
[oidcClientId],
`The token aud value doesn't match the oidc_CLIENT_ID (${oidcClientId})`
.mixed()
.test(
'aud-test',
`The token aud value doesn't match the oidc_CLIENT_ID (${oidcClientId})`,
(value) => {
const audience = Array.isArray(value) ? value : [value];
return audience.includes(oidcClientId);
}
)
.required("The token didn't come with an aud value."),
exp: yup
.number()
.required()
@ -64,11 +70,12 @@ export const createJwtSchema = ({
'is_before_date',
'Token exp value is before current time.',
(value) => {
if (!value) return false;
if (value < Math.ceil(Date.now() / 1000)) return false;
return true;
// Check if 'value' is undefined
if (value === undefined) return false;
return value >= Math.ceil(Date.now() / 1000);
}
),
iat: yup
.number()
.required()
@ -76,14 +83,13 @@ export const createJwtSchema = ({
'is_before_one_day',
'Token was issued before one day ago and is now invalid.',
(value) => {
if (!value) return false;
const date = new Date();
date.setDate(date.getDate() - 1);
if (value < Math.ceil(Number(date) / 1000)) return false;
return true;
// Check if 'value' is undefined
if (value === undefined) return false;
const oneDayAgo = Math.ceil(Number(new Date()) / 1000) - 86400;
return value >= oneDayAgo;
}
),
// these should exist because we set the scope to `openid profile email`
email: yup.string().email().required(),
email_verified: yup.boolean().required(),
});
@ -109,3 +115,29 @@ export interface WellKnownConfiguration {
claims_supported: string[];
request_uri_parameter_supported: boolean;
}
export interface OIDCJwtPayload {
// Standard OIDC Claims
iss: string; // Issuer Identifier
sub: string; // Subject Identifier
aud: string | string[]; // Audience
exp: number; // Expiration time
iat: number; // Issued at time
auth_time?: number; // Time when the authentication occurred (optional)
nonce?: string; // String value used to associate a Client session with an ID Token (optional)
// Commonly used OIDC Claims
email?: string; // User's email address (optional)
email_verified?: boolean; // Whether the user's email address has been verified (optional)
name?: string; // User's full name (optional)
given_name?: string; // User's given name(s) or first name(s) (optional)
family_name?: string; // User's surname(s) or last name(s) (optional)
preferred_username?: string; // Shorthand name by which the user wishes to be referred to (optional)
locale?: string; // User's locale (optional)
zoneinfo?: string; // User's time zone (optional)
// Other possible custom claims (these depend on your OIDC provider)
// Include any additional fields that your OIDC provider might use
[additionalClaim: string]: unknown;
}

Loading…
Cancel
Save