fix: callback URL validation not validated if prompt=none

This commit is contained in:
Elias Schneider
2026-06-16 12:02:57 +02:00
parent 73bda4580a
commit 8a75774971
7 changed files with 170 additions and 18 deletions
+15
View File
@@ -1,6 +1,7 @@
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
import type {
AccessibleOidcClient,
AuthorizeCallbackResponse,
AuthorizeResponse,
OidcClient,
OidcClientCreate,
@@ -44,6 +45,20 @@ class OidcService extends APIService {
return res.data as AuthorizeResponse;
};
resolveAuthorizeCallbackURL = async (
clientId: string,
callbackURL: string,
requestURI?: string
) => {
const res = await this.api.post('/oidc/authorize/callback-url', {
clientId,
callbackURL,
requestURI
});
return res.data as AuthorizeCallbackResponse;
};
isAuthorizationRequired = async (clientId: string, scope: string, requestURI?: string) => {
const res = await this.api.post('/oidc/authorization-required', {
scope,
+4
View File
@@ -70,6 +70,10 @@ export type AuthorizeResponse = {
requiresRedirect?: boolean;
};
export type AuthorizeCallbackResponse = {
callbackURL: string;
};
export type AccessibleOidcClient = OidcClientMetaData & {
lastUsedAt: Date | null;
};
+31 -16
View File
@@ -12,7 +12,7 @@
import appConfigStore from '$lib/stores/application-configuration-store';
import userStore from '$lib/stores/user-store';
import { cachedProfilePicture } from '$lib/utils/cached-image-util';
import { getWebauthnErrorMessage } from '$lib/utils/error-util';
import { getAxiosErrorMessage, getWebauthnErrorMessage } from '$lib/utils/error-util';
import { startAuthentication, type AuthenticationResponseJSON } from '@simplewebauthn/browser';
import { onMount } from 'svelte';
import { slide } from 'svelte/transition';
@@ -64,15 +64,19 @@
const hasPromptSelectAccount = promptValues.includes('select_account');
onMount(() => {
void handleInitialAuthorization();
});
async function handleInitialAuthorization() {
// Conflicting prompt values - none can't be combined with any interactive prompt
if (hasPromptNone && (hasPromptConsent || hasPromptLogin || hasPromptSelectAccount)) {
redirectWithError('interaction_required');
await redirectWithError('interaction_required');
return;
}
// If prompt=none and user is not signed in, redirect immediately with login_required
if (hasPromptNone && !$userStore) {
redirectWithError('login_required');
await redirectWithError('login_required');
return;
}
@@ -85,9 +89,9 @@
}
if ($userStore) {
authorize();
await authorize();
}
});
}
async function useDifferentAccount() {
try {
@@ -130,7 +134,7 @@
// If prompt=none and consent required, redirect with error
if (hasPromptNone && authorizationRequired) {
redirectWithError('consent_required');
await redirectWithError('consent_required');
return;
}
@@ -169,7 +173,7 @@
// Check if backend returned a redirect error
if (result.requiresRedirect && result.error) {
if (hasPromptNone) {
redirectWithError(result.error);
await redirectWithError(result.error, result.callbackURL);
} else {
errorMessage = result.error;
isLoading = false;
@@ -184,16 +188,27 @@
}
}
function redirectWithError(error: string) {
const redirectURL = new URL(callbackURL);
if (redirectURL.protocol == 'javascript:' || redirectURL.protocol == 'data:') {
throw new Error('Invalid redirect URL protocol');
}
async function redirectWithError(error: string, validatedCallbackURL?: string) {
isLoading = true;
window.location.href = createRedirectURL(callbackURL, {
error,
state: authorizeState
});
try {
const safeCallbackURL =
validatedCallbackURL ||
(await oidService.resolveAuthorizeCallbackURL(client!.id, callbackURL, requestURI))
.callbackURL;
const redirectURL = new URL(safeCallbackURL);
if (redirectURL.protocol == 'javascript:' || redirectURL.protocol == 'data:') {
throw new Error('Invalid redirect URL protocol');
}
window.location.href = createRedirectURL(safeCallbackURL, {
error,
state: authorizeState
});
} catch (e) {
errorMessage = getAxiosErrorMessage(e);
isLoading = false;
}
}
function onSuccess(code: string, callbackURL: string, issuer: string) {