1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-02-10 21:54:19 +00:00

feat: add option to OIDC client to require re-authentication (#747)

Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Robert Mang
2025-08-22 08:56:40 +02:00
committed by GitHub
parent 7ab0fd3028
commit 0cb039d35d
22 changed files with 362 additions and 44 deletions

View File

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

View File

@@ -37,6 +37,11 @@ class WebAuthnService extends APIService {
async updateCredentialName(id: string, name: string) {
await this.api.patch(`/webauthn/credentials/${id}`, { name });
}
async reauthenticate(body?: AuthenticationResponseJSON) {
const res = await this.api.post('/webauthn/reauthenticate', body);
return res.data.reauthenticationToken as string;
}
}
export default WebAuthnService;

View File

@@ -4,6 +4,7 @@ export type OidcClientMetaData = {
id: string;
name: string;
hasLogo: boolean;
requiresReauthentication: boolean;
launchURL?: string;
};
@@ -23,6 +24,7 @@ export type OidcClient = OidcClientMetaData & {
logoutCallbackURLs: string[];
isPublic: boolean;
pkceEnabled: boolean;
requiresReauthentication: boolean;
credentials?: OidcClientCredentials;
launchURL?: string;
};

View File

@@ -11,7 +11,7 @@
import userStore from '$lib/stores/user-store';
import { getWebauthnErrorMessage } from '$lib/utils/error-util';
import { LucideMail, LucideUser, LucideUsers } from '@lucide/svelte';
import { startAuthentication } from '@simplewebauthn/browser';
import { startAuthentication, type AuthenticationResponseJSON } from '@simplewebauthn/browser';
import { onMount } from 'svelte';
import { slide } from 'svelte/transition';
import type { PageProps } from './$types';
@@ -29,6 +29,7 @@
let errorMessage: string | null = $state(null);
let authorizationRequired = $state(false);
let authorizationConfirmed = $state(false);
let userSignedInAt: Date | undefined;
onMount(() => {
if ($userStore) {
@@ -38,13 +39,16 @@
async function authorize() {
isLoading = true;
let authResponse: AuthenticationResponseJSON | undefined;
try {
// Get access token if not signed in
if (!$userStore?.id) {
const loginOptions = await webauthnService.getLoginOptions();
const authResponse = await startAuthentication({ optionsJSON: loginOptions });
authResponse = await startAuthentication({ optionsJSON: loginOptions });
const user = await webauthnService.finishLogin(authResponse);
await userStore.setUser(user);
userStore.setUser(user);
userSignedInAt = new Date();
}
if (!authorizationConfirmed) {
@@ -56,8 +60,28 @@
}
}
let reauthToken: string | undefined;
if (client?.requiresReauthentication) {
let authResponse;
const signedInRecently =
userSignedInAt && userSignedInAt.getTime() > Date.now() - 60 * 1000;
if (!signedInRecently) {
const loginOptions = await webauthnService.getLoginOptions();
authResponse = await startAuthentication({ optionsJSON: loginOptions });
}
reauthToken = await webauthnService.reauthenticate(authResponse);
}
await oidService
.authorize(client!.id, scope, callbackURL, nonce, codeChallenge, codeChallengeMethod)
.authorize(
client!.id,
scope,
callbackURL,
nonce,
codeChallenge,
codeChallengeMethod,
reauthToken
)
.then(async ({ code, callbackURL, issuer }) => {
onSuccess(code, callbackURL, issuer);
});

View File

@@ -36,7 +36,8 @@
[m.userinfo_url()]: `https://${page.url.hostname}/api/oidc/userinfo`,
[m.logout_url()]: `https://${page.url.hostname}/api/oidc/end-session`,
[m.certificate_url()]: `https://${page.url.hostname}/.well-known/jwks.json`,
[m.pkce()]: client.pkceEnabled ? m.enabled() : m.disabled()
[m.pkce()]: client.pkceEnabled ? m.enabled() : m.disabled(),
[m.requires_reauthentication()]: client.requiresReauthentication ? m.enabled() : m.disabled()
});
async function updateClient(updatedClient: OidcClientCreateWithLogo) {
@@ -49,6 +50,9 @@
client.isPublic = updatedClient.isPublic;
setupDetails[m.pkce()] = updatedClient.pkceEnabled ? m.enabled() : m.disabled();
setupDetails[m.requires_reauthentication()] = updatedClient.requiresReauthentication
? m.enabled()
: m.disabled();
await Promise.all([dataPromise, imagePromise])
.then(() => {
@@ -120,14 +124,14 @@
<Card.Content>
<div class="flex flex-col">
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
<Label class="mb-0 w-44">{m.client_id()}</Label>
<Label class="mb-0 w-50">{m.client_id()}</Label>
<CopyToClipboard value={client.id}>
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
</CopyToClipboard>
</div>
{#if !client.isPublic}
<div class="mt-1 mb-2 flex flex-col sm:flex-row sm:items-center">
<Label class="mb-0 w-44">{m.client_secret()}</Label>
<Label class="mb-0 w-50">{m.client_secret()}</Label>
{#if $clientSecretStore}
<CopyToClipboard value={$clientSecretStore}>
<span class="text-muted-foreground text-sm" data-testid="client-secret">
@@ -154,7 +158,7 @@
<div transition:slide>
{#each Object.entries(setupDetails) as [key, value]}
<div class="mb-5 flex flex-col sm:flex-row sm:items-center">
<Label class="mb-0 w-44">{key}</Label>
<Label class="mb-0 w-50">{key}</Label>
<CopyToClipboard {value}>
<span class="text-muted-foreground text-sm">{value}</span>
</CopyToClipboard>

View File

@@ -39,6 +39,7 @@
logoutCallbackURLs: existingClient?.logoutCallbackURLs || [],
isPublic: existingClient?.isPublic || false,
pkceEnabled: existingClient?.pkceEnabled || false,
requiresReauthentication: existingClient?.requiresReauthentication || false,
launchURL: existingClient?.launchURL || '',
credentials: {
federatedIdentities: existingClient?.credentials?.federatedIdentities || []
@@ -51,6 +52,7 @@
logoutCallbackURLs: z.array(z.string().nonempty()),
isPublic: z.boolean(),
pkceEnabled: z.boolean(),
requiresReauthentication: z.boolean(),
launchURL: optionalUrl,
credentials: z.object({
federatedIdentities: z.array(
@@ -147,6 +149,12 @@
description={m.public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks()}
bind:checked={$inputs.pkceEnabled.value}
/>
<SwitchWithLabel
id="requires-reauthentication"
label={m.requires_reauthentication()}
description={m.requires_users_to_authenticate_again_on_each_authorization()}
bind:checked={$inputs.requiresReauthentication.value}
/>
</div>
<div class="mt-8">
<Label for="logo">{m.logo()}</Label>