mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-05-19 03:19:53 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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:') {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user