feat: add OpenID Connect prompt Parameter Handling (#1299)

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Robert Jaakke
2026-04-19 18:03:08 +02:00
committed by GitHub
parent 4f09de2cfc
commit 59fe481af9
12 changed files with 447 additions and 28 deletions

View File

@@ -22,7 +22,8 @@ class OidcService extends APIService {
nonce?: string,
codeChallenge?: string,
codeChallengeMethod?: string,
reauthenticationToken?: string
reauthenticationToken?: string,
prompt?: string
) => {
const res = await this.api.post('/oidc/authorize', {
scope,
@@ -31,7 +32,8 @@ class OidcService extends APIService {
clientId,
codeChallenge,
codeChallengeMethod,
reauthenticationToken
reauthenticationToken,
prompt
});
return res.data as AuthorizeResponse;

View File

@@ -62,9 +62,11 @@ export type OidcDeviceCodeInfo = {
};
export type AuthorizeResponse = {
code: string;
callbackURL: string;
issuer: string;
code?: string;
callbackURL?: string;
issuer?: string;
error?: string;
requiresRedirect?: boolean;
};
export type AccessibleOidcClient = OidcClientMetaData & {

View File

@@ -20,7 +20,7 @@
const oidService = new OidcService();
let { data }: PageProps = $props();
let { client, scope, callbackURL, nonce, codeChallenge, codeChallengeMethod, authorizeState } =
let { client, scope, callbackURL, nonce, codeChallenge, codeChallengeMethod, authorizeState, prompt } =
data;
let isLoading = $state(false);
@@ -30,7 +30,26 @@
let authorizationConfirmed = $state(false);
let userSignedInAt: Date | undefined;
// Parse prompt parameter once (space-delimited per OIDC spec)
const promptValues = prompt ? prompt.split(' ') : [];
const hasPromptNone = promptValues.includes('none');
const hasPromptConsent = promptValues.includes('consent');
const hasPromptLogin = promptValues.includes('login');
const hasPromptSelectAccount = promptValues.includes('select_account');
onMount(() => {
// Conflicting prompt values - none can't be combined with any interactive prompt
if (hasPromptNone && (hasPromptConsent || hasPromptLogin || hasPromptSelectAccount)) {
redirectWithError('interaction_required');
return;
}
// If prompt=none and user is not signed in, redirect immediately with login_required
if (hasPromptNone && !$userStore) {
redirectWithError('login_required');
return;
}
if ($userStore) {
authorize();
}
@@ -52,6 +71,18 @@
if (!authorizationConfirmed) {
authorizationRequired = await oidService.isAuthorizationRequired(client!.id, scope);
// If prompt=consent, always show consent UI
if (hasPromptConsent) {
authorizationRequired = true;
}
// If prompt=none and consent required, redirect with error
if (hasPromptNone && authorizationRequired) {
redirectWithError('consent_required');
return;
}
if (authorizationRequired) {
isLoading = false;
authorizationConfirmed = true;
@@ -60,7 +91,7 @@
}
let reauthToken: string | undefined;
if (client?.requiresReauthentication) {
if (client?.requiresReauthentication || hasPromptLogin) {
let authResponse;
const signedInRecently =
userSignedInAt && userSignedInAt.getTime() > Date.now() - 60 * 1000;
@@ -71,22 +102,48 @@
reauthToken = await webauthnService.reauthenticate(authResponse);
}
const authResult = await oidService.authorize(
const result = await oidService.authorize(
client!.id,
scope,
callbackURL,
nonce,
codeChallenge,
codeChallengeMethod,
reauthToken
reauthToken,
prompt
);
onSuccess(authResult.code, authResult.callbackURL, authResult.issuer);
// Check if backend returned a redirect error
if (result.requiresRedirect && result.error) {
if (hasPromptNone) {
redirectWithError(result.error);
} else {
errorMessage = result.error;
isLoading = false;
}
return;
}
onSuccess(result.code!, result.callbackURL!, result.issuer!);
} catch (e) {
errorMessage = getWebauthnErrorMessage(e);
isLoading = false;
}
}
function redirectWithError(error: string) {
const redirectURL = new URL(callbackURL);
if (redirectURL.protocol == 'javascript:' || redirectURL.protocol == 'data:') {
throw new Error('Invalid redirect URL protocol');
}
redirectURL.searchParams.append('error', error);
if (authorizeState) {
redirectURL.searchParams.append('state', authorizeState);
}
window.location.href = redirectURL.toString();
}
function onSuccess(code: string, callbackURL: string, issuer: string) {
const redirectURL = new URL(callbackURL);
if (redirectURL.protocol == 'javascript:' || redirectURL.protocol == 'data:') {

View File

@@ -14,6 +14,7 @@ export const load: PageLoad = async ({ url }) => {
callbackURL: url.searchParams.get('redirect_uri')!,
client,
codeChallenge: url.searchParams.get('code_challenge')!,
codeChallengeMethod: url.searchParams.get('code_challenge_method')!
codeChallengeMethod: url.searchParams.get('code_challenge_method')!,
prompt: url.searchParams.get('prompt') || undefined
};
};