Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Comments

feat: API support for OpenID Connect authentication#2474

Draft
michaelhthomas wants to merge 10 commits intoseerr-team:developfrom
michaelhthomas:feat/oidc-login-api
Draft

feat: API support for OpenID Connect authentication#2474
michaelhthomas wants to merge 10 commits intoseerr-team:developfrom
michaelhthomas:feat/oidc-login-api

Conversation

@michaelhthomas
Copy link
Contributor

Description

  • Fixes #XXXX

How Has This Been Tested?

Screenshots / Logs (if applicable)

Checklist:

  • I have read and followed the contributionguidelines.
  • Disclosed any use of AI (see ourpolicy)
  • I have updated the documentation accordingly.
  • All new and existing tests passed.
  • Successful buildpnpm build
  • Translation keyspnpm i18n:extract
  • Database migration (if required)

maarten-vdb and smevawala reacted with heart emoji
@coderabbitai
Copy link

coderabbitaibot commentedFeb 17, 2026
edited
Loading

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the.coderabbit.yaml file in this repository. To trigger a single review, invoke the@coderabbitai review command.

You can disable this status message by setting thereviews.review_status tofalse in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment@coderabbitai help to get the list of available commands and usage tips.

@michaelhthomasmichaelhthomas changed the titledraft: API support for OIDC loginfeat: API support for OpenID Connect authenticationFeb 17, 2026
const userinfo = options?.userinfoResponse ?? DEFAULT_CLAIMS;
const idTokenClaims = options?.idTokenClaims;

fetchMocker.mockIf(/example\.com/, async (req) => {

Check failure

Code scanning / CodeQL

Missing regular expression anchor

When this is used as a regular expression on a URL, it may match anywhere, and arbitrary hosts may come before or after it.
Comment on lines 652 to 716
authRoutes.get('/oidc/login/:slug', async (req, res, next) => {
const settings = getSettings();
const provider = settings.oidc.providers.find(
(p) => p.slug === req.params.slug
);

if (!settings.main.oidcLogin || !provider) {
return next({
status: 403,
message: 'OpenID Connect sign-in is disabled.',
});
}

const config = await openIdClient.discovery(
new URL(provider.issuerUrl),
provider.clientId,
provider.clientSecret
);

const code_verifier = openIdClient.randomPKCECodeVerifier();
const code_challenge = await openIdClient.calculatePKCECodeChallenge(
code_verifier
);
res.cookie('oidc-code-verifier', code_verifier, {
maxAge: 60000,
httpOnly: true,
secure: req.protocol === 'https',
});

const callbackUrl = new URL(
`/login`,
settings.main.applicationUrl ?? `${req.protocol}://${req.headers.host}`
);
callbackUrl.searchParams.set('provider', provider.slug);
callbackUrl.searchParams.set('callback', 'true');

const parameters: Record<string, string> = {
redirect_uri: callbackUrl.toString(),
scope: provider.scopes ?? 'openid profile email',
code_challenge,
code_challenge_method: 'S256',
};

/**
* We cannot be sure the server supports PKCE so we're going to use state too.
* Use of PKCE is backwards compatible even if the AS doesn't support it which
* is why we're using it regardless. Like PKCE, random state must be generated
* for every redirect to the authorization_endpoint.
*/
if (!config.serverMetadata().supportsPKCE()) {
const state = openIdClient.randomState();
parameters.state = state;
res.cookie('oidc-state', state, {
maxAge: 60000,
httpOnly: true,
secure: req.protocol === 'https',
});
}

const redirectUrl = openIdClient.buildAuthorizationUrl(config, parameters);

return res.status(200).json({
redirectUrl,
});
});

Check failure

Code scanning / CodeQL

Missing rate limiting

This route handler performs [authorization](1), but is not rate-limited.
Comment on lines 718 to 902
authRoutes.get('/oidc/callback/:slug', async (req, res, next) => {
const settings = getSettings();
const provider = settings.oidc.providers.find(
(p) => p.slug === req.params.slug
);

if (!settings.main.oidcLogin || !provider) {
return next({
status: 403,
message: 'OpenID Connect sign-in is disabled',
});
}

try {
const config = await openIdClient.discovery(
new URL(provider.issuerUrl),
provider.clientId,
provider.clientSecret
);

const pkceCodeVerifier = req.cookies['oidc-code-verifier'];
const expectedState = req.cookies['oidc-state'];

const tokens = await openIdClient.authorizationCodeGrant(
config,
new URL(req.url, `${req.protocol}://${req.hostname}`),
{
pkceCodeVerifier,
expectedState,
}
);

const claims = tokens.claims();
if (claims == null) {
logger.info('Failed OIDC login attempt', {
cause:
'Missing ID token in response. Provider does not support OpenID Connect.',
ip: req.ip,
provider: provider.name,
});

return next({
status: 400,
message: 'Invalid token response',
});
}

const requiredClaims = (provider.requiredClaims ?? '')
.split(' ')
.filter((s) => !!s);

const userInfo = await openIdClient.fetchUserInfo(
config,
tokens.access_token,
claims.sub
);

const fullUserInfo = { ...claims, ...userInfo };

// Validate that user meets required claims
const hasRequiredClaims = requiredClaims.every((claim) => {
const value = userInfo[claim];
return value === true;
});

if (!hasRequiredClaims) {
logger.info('Failed OIDC login attempt', {
cause: 'Failed to validate required claims',
ip: req.ip,
requiredClaims: provider.requiredClaims,
});
return next({
status: 403,
message: 'Insufficient permissions',
});
}

// Map identifier to linked account
const userRepository = getRepository(User);
const linkedAccountsRepository = getRepository(LinkedAccount);

const linkedAccount = await linkedAccountsRepository.findOne({
relations: {
user: true,
},
where: {
provider: provider.slug,
sub: fullUserInfo.sub,
},
});
let user = linkedAccount?.user;

// If there is already a user logged in, and no linked account, link the account.
if (req.user != null && linkedAccount == null) {
const linkedAccount = new LinkedAccount({
user: req.user,
provider: provider.slug,
sub: fullUserInfo.sub,
username: fullUserInfo.preferred_username ?? req.user.displayName,
});

await linkedAccountsRepository.save(linkedAccount);
return res
.status(200)
.json({ status: 'ok', to: '/profile/settings/linked-accounts' });
}

// Create user if one doesn't already exist
if (!user && fullUserInfo.email != null && provider.newUserLogin) {
// Check if a user with this email already exists
const existingUser = await userRepository.findOne({
where: { email: fullUserInfo.email },
});

if (existingUser) {
// If a user with the email exists, throw a 409 Conflict error
return next({
status: 409,
message: 'A user with this email address already exists.',
});
}

logger.info(`Creating user for ${fullUserInfo.email}`, {
ip: req.ip,
email: fullUserInfo.email,
});

const avatar =
fullUserInfo.picture ??
gravatarUrl(fullUserInfo.email, { default: 'mm', size: 200 });
user = new User({
avatar: avatar,
username: fullUserInfo.preferred_username,
email: fullUserInfo.email,
permissions: settings.main.defaultPermissions,
plexToken: '',
userType: UserType.LOCAL,
});
await userRepository.save(user);

const linkedAccount = new LinkedAccount({
user,
provider: provider.slug,
sub: fullUserInfo.sub,
username: fullUserInfo.preferred_username ?? fullUserInfo.email,
});
await linkedAccountsRepository.save(linkedAccount);

user.linkedAccounts = [linkedAccount];
await userRepository.save(user);
}

if (!user) {
logger.debug('Failed OIDC sign-up attempt', {
cause: provider.newUserLogin
? 'User did not have an account, and was missing an associated email address.'
: 'User did not have an account, and new user login was disabled.',
});
return next({
status: 400,
message: provider.newUserLogin
? 'Unable to create new user account (missing email address)'
: 'Unable to create new user account (new user login is disabled)',
});
}

// Set logged in session and return
if (req.session) {
req.session.userId = user.id;
}

// Success!
return res.status(200).json({ status: 'ok', to: '/' });
} catch (error) {
logger.error('Failed OIDC login attempt', {
cause: 'Unknown error',
ip: req.ip,
error,
});
return next({
status: 500,
message: 'An unknown error occurred',
});
}
});

Check failure

Code scanning / CodeQL

Missing rate limiting

This route handler performs [authorization](1), but is not rate-limited.
TypeORM represents the 'date' column type (date only) as a string, not a JS date object. This wascausing  the reset password expiration check to never activate, since it compares the date (which isa string) with new Date(), which is always truthy.
@michaelhthomasmichaelhthomasforce-pushed thefeat/oidc-login-api branch 2 times, most recently from0e9863e to8c6741cCompareFebruary 18, 2026 22:39
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant

@michaelhthomas

[8]ページ先頭

©2009-2026 Movatter.jp