feat: restore old behavior of automatically creating client secret

This commit is contained in:
Elias Schneider
2026-09-23 22:00:43 +02:00
parent 2075de3234
commit d259a15131
19 changed files with 240 additions and 40 deletions
+2 -1
View File
@@ -7,6 +7,7 @@ import type {
InteractionStep,
OidcClient,
OidcClientCreate,
OidcClientCreated,
OidcClientMetaData,
OidcClientSecret,
OidcClientSecretCreated,
@@ -42,7 +43,7 @@ class OidcService extends APIService {
};
createClient = async (client: OidcClientCreate) =>
(await this.api.post('/oidc/clients', client)).data as OidcClient;
(await this.api.post('/oidc/clients', client)).data as OidcClientCreated;
removeClient = async (id: string) => {
await this.api.delete(`/oidc/clients/${encodeClientIdParam(id)}`);
@@ -3,6 +3,7 @@ import { writable } from 'svelte/store';
// Holds the clear-text value of the client secrets created during the current page visit, keyed by secret ID.
// The server never returns those values again, so they are shown until the user navigates away and then forgotten.
const clientSecretStore = writable<Record<string, string>>({});
export const autoCreatedSecretId = writable<string | null>(null);
const set = (secretId: string, secret: string) => {
clientSecretStore.update((secrets) => ({ ...secrets, [secretId]: secret }));
@@ -18,11 +19,18 @@ const remove = (secretId: string) => {
const clear = () => {
clientSecretStore.set({});
autoCreatedSecretId.set(null);
};
const setAutoCreated = (secretId: string, secret: string) => {
set(secretId, secret);
autoCreatedSecretId.set(secretId);
};
export default {
subscribe: clientSecretStore.subscribe,
set,
setAutoCreated,
remove,
clear
};
@@ -58,6 +58,7 @@ export type AllAppConfig = AppConfig & {
webauthnAuthenticatorAttachment: 'any' | 'platform' | 'cross-platform';
// OIDC
cimdUrlAllowlist: string[];
autoCreateOidcClientSecret: boolean;
};
export type AppConfigRawResponse = {
+4
View File
@@ -68,6 +68,10 @@ export type OidcClient = OidcClientMetaData & {
refreshTokenDurationMinutes: number;
};
export type OidcClientCreated = OidcClient & {
createdSecret?: OidcClientSecretCreated;
};
export type OidcClientTokenLifetimes = Pick<
OidcClient,
'accessTokenDurationMinutes' | 'refreshTokenDurationMinutes'
@@ -8,6 +8,7 @@
import type { AllAppConfig } from '$lib/types/application-configuration.type';
import { LucideInfo } from '@lucide/svelte';
import AppConfigDynamicClientsForm from './forms/app-config-dynamic-clients-form.svelte';
import AppConfigClientSecretsForm from './forms/app-config-client-secrets-form.svelte';
import AppConfigEmailForm from './forms/app-config-email-form.svelte';
import AppConfigGeneralForm from './forms/app-config-general-form.svelte';
import AppConfigLdapForm from './forms/app-config-ldap-form.svelte';
@@ -191,7 +192,16 @@
</Card.Root>
</Tabs.Content>
<Tabs.Content value="oidc" id="application-configuration-oidc">
<Tabs.Content value="oidc" id="application-configuration-oidc" class="flex flex-col gap-4">
<Card.Root>
<Card.Header>
<Card.Title>{m.general()}</Card.Title>
</Card.Header>
<Card.Content>
<AppConfigClientSecretsForm {appConfig} callback={updateAppConfig} />
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title>{m.client_id_metadata_documents()}</Card.Title>
@@ -0,0 +1,34 @@
<script lang="ts">
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
import { m } from '$lib/paraglide/messages';
import appConfigStore from '$lib/stores/application-configuration-store';
import type { AllAppConfig } from '$lib/types/application-configuration.type';
import { createForm } from '$lib/utils/form-util';
import { trackFormChanges } from '$lib/utils/unsaved-changes-util.svelte';
import { z } from 'zod/v4';
let {
appConfig,
callback
}: {
appConfig: AllAppConfig;
callback: (appConfig: Partial<AllAppConfig>) => Promise<void>;
} = $props();
const formSchema = z.object({ autoCreateOidcClientSecret: z.boolean() });
let formStore = $derived(
createForm(formSchema, { autoCreateOidcClientSecret: appConfig.autoCreateOidcClientSecret })
);
let inputs = $derived(formStore.inputs);
trackFormChanges(() => formStore, callback);
</script>
<fieldset disabled={$appConfigStore.uiConfigDisabled}>
<SwitchWithLabel
id="auto-create-oidc-client-secret"
label={m.auto_create_client_secret()}
description={m.auto_create_client_secret_description()}
bind:checked={$inputs.autoCreateOidcClientSecret.value}
/>
</fieldset>
@@ -21,6 +21,12 @@
async function createOIDCClient(client: OidcClientCreateWithLogo) {
clientSecretStore.clear();
const createdClient = await oidcService.createClient(client);
if (createdClient.createdSecret) {
clientSecretStore.setAutoCreated(
createdClient.createdSecret.id,
createdClient.createdSecret.secret
);
}
const logoPromise = client.logo
? oidcService.updateClientLogo(createdClient, client.logo, true)
@@ -30,7 +36,6 @@
: Promise.resolve();
await Promise.all([logoPromise, darkLogoPromise]);
// A new client starts without any secret: the admin creates the ones they need from the credentials tab
goto(`/settings/admin/oidc-clients/${encodeClientIdParam(createdClient.id)}`);
toast.success(m.oidc_client_created_successfully());
}
@@ -11,7 +11,7 @@
import { m } from '$lib/paraglide/messages';
import OidcService from '$lib/services/oidc-service';
import ScimService from '$lib/services/scim-service';
import clientSecretStore from '$lib/stores/client-secret-store';
import clientSecretStore, { autoCreatedSecretId } from '$lib/stores/client-secret-store';
import type {
OidcClientCreateWithLogo,
OidcClientCredentials,
@@ -251,6 +251,16 @@
</span>
</CopyToClipboard>
</div>
{#if $autoCreatedSecretId && clientSecrets.some((secret) => secret.id === $autoCreatedSecretId) && $clientSecretStore[$autoCreatedSecretId]}
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
<Field.Label class="w-52">{m.client_secret()}</Field.Label>
<CopyToClipboard value={$clientSecretStore[$autoCreatedSecretId]}>
<span class="text-muted-foreground text-sm break-all" data-testid="client-secret">
{$clientSecretStore[$autoCreatedSecretId]}
</span>
</CopyToClipboard>
</div>
{/if}
{#if showAllDetails}
<div transition:slide>
{#each Object.entries(setupDetails) as [key, value] (key)}