feat(oauth): add support for Pushed Authorization Requests (RFC9126) (#1404)

Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Thibault NORMAND
2026-06-02 14:02:12 +02:00
committed by GitHub
co-authored by Elias Schneider
parent 2eada149af
commit 68a5abdcca
27 changed files with 765 additions and 54 deletions
+8 -5
View File
@@ -24,7 +24,8 @@ class OidcService extends APIService {
codeChallengeMethod?: string,
reauthenticationToken?: string,
responseMode?: string,
prompt?: string
prompt?: string,
requestURI?: string
) => {
const res = await this.api.post('/oidc/authorize', {
scope,
@@ -35,19 +36,21 @@ class OidcService extends APIService {
codeChallengeMethod,
reauthenticationToken,
responseMode,
prompt
prompt,
requestURI
});
return res.data as AuthorizeResponse;
};
isAuthorizationRequired = async (clientId: string, scope: string) => {
isAuthorizationRequired = async (clientId: string, scope: string, requestURI?: string) => {
const res = await this.api.post('/oidc/authorization-required', {
scope,
clientId
clientId,
requestURI
});
return res.data.authorizationRequired as boolean;
return res.data as { authorizationRequired: boolean; scope: string };
};
listClients = async (options?: ListRequestOptions) => {
+1
View File
@@ -26,6 +26,7 @@ export type OidcClient = OidcClientMetaData & {
isPublic: boolean;
pkceEnabled: boolean;
requiresReauthentication: boolean;
requiresPushedAuthorizationRequests: boolean;
credentials?: OidcClientCredentials;
launchURL?: string;
isGroupRestricted: boolean;
+15 -5
View File
@@ -25,16 +25,16 @@
let { data }: PageProps = $props();
let {
client,
scope,
callbackURL,
nonce,
codeChallenge,
codeChallengeMethod,
authorizeState,
prompt,
responseMode
responseMode,
requestURI
} = data;
let scope = $state(data.scope);
let isLoading = $state(false);
let success = $state(false);
let errorMessage: string | null = $state(null);
@@ -112,7 +112,16 @@
}
if (!authorizationConfirmed) {
authorizationRequired = await oidService.isAuthorizationRequired(client!.id, scope);
const authRequired = await oidService.isAuthorizationRequired(
client!.id,
scope,
requestURI
);
authorizationRequired = authRequired.authorizationRequired;
if (requestURI) {
scope = authRequired.scope;
}
// If prompt=consent, always show consent UI
if (hasPromptConsent) {
@@ -153,7 +162,8 @@
codeChallengeMethod,
reauthToken,
responseMode,
prompt
prompt,
requestURI
);
// Check if backend returned a redirect error
+2 -1
View File
@@ -16,6 +16,7 @@ export const load: PageLoad = async ({ url }) => {
codeChallenge: url.searchParams.get('code_challenge')!,
codeChallengeMethod: url.searchParams.get('code_challenge_method')!,
prompt: url.searchParams.get('prompt') || undefined,
responseMode: url.searchParams.get('response_mode') || undefined
responseMode: url.searchParams.get('response_mode') || undefined,
requestURI: url.searchParams.get('request_uri') || undefined
};
};
@@ -47,7 +47,10 @@
[m.logout_url()]: `https://${page.url.host}/api/oidc/end-session`,
[m.certificate_url()]: `https://${page.url.host}/.well-known/jwks.json`,
[m.pkce()]: client.pkceEnabled ? m.enabled() : m.disabled(),
[m.requires_reauthentication()]: client.requiresReauthentication ? m.enabled() : m.disabled()
[m.requires_reauthentication()]: client.requiresReauthentication ? m.enabled() : m.disabled(),
[m.requires_pushed_authorization_requests()]: client.requiresPushedAuthorizationRequests
? m.enabled()
: m.disabled()
});
async function updateClient(updatedClient: OidcClientCreateWithLogo) {
@@ -71,6 +74,8 @@
await Promise.all([dataPromise, imagePromise, darkImagePromise])
.then(() => {
setupDetails[m.requires_pushed_authorization_requests()] =
updatedClient.requiresPushedAuthorizationRequests ? m.enabled() : m.disabled();
if (updatedClient.logoUrl) {
cachedOidcClientLogo.bustCache(client.id, true);
}
@@ -49,6 +49,8 @@
isPublic: existingClient?.isPublic || false,
pkceEnabled: existingClient?.pkceEnabled || false,
requiresReauthentication: existingClient?.requiresReauthentication || false,
requiresPushedAuthorizationRequests:
existingClient?.requiresPushedAuthorizationRequests || false,
launchURL: existingClient?.launchURL || '',
credentials: {
federatedIdentities: existingClient?.credentials?.federatedIdentities || []
@@ -74,6 +76,7 @@
isPublic: z.boolean(),
pkceEnabled: z.boolean(),
requiresReauthentication: z.boolean(),
requiresPushedAuthorizationRequests: z.boolean(),
launchURL: optionalUrl,
logoUrl: optionalUrl,
darkLogoUrl: optionalUrl,
@@ -205,6 +208,7 @@
onCheckedChange={(v) => {
if (v) {
$inputs.pkceEnabled.value = true;
$inputs.requiresPushedAuthorizationRequests.value = false;
}
}}
bind:checked={$inputs.isPublic.value}
@@ -270,6 +274,13 @@
{#if showAdvancedOptions}
<div class="mt-7 flex flex-col gap-y-7 md:col-span-2" transition:slide={{ duration: 200 }}>
<SwitchWithLabel
id="requires-par"
label={m.requires_pushed_authorization_requests()}
description={m.requires_pushed_authorization_requests_description()}
disabled={$inputs.isPublic.value}
bind:checked={$inputs.requiresPushedAuthorizationRequests.value}
/>
{#if mode == 'create'}
<FormInput
label={m.client_id()}
@@ -59,6 +59,13 @@
sortable: true,
filterableValues: booleanFilterValues
},
{
label: m.par(),
column: 'requiresPushedAuthorizationRequests',
sortable: true,
hidden: true,
filterableValues: booleanFilterValues
},
{
label: m.client_launch_url(),
column: 'launchURL',