mirror of
https://github.com/nikdoof/pocket-id.git
synced 2025-12-14 07:12:19 +00:00
feat: add PKCE support
This commit is contained in:
@@ -3,23 +3,27 @@ import type { Paginated, PaginationRequest } from '$lib/types/pagination.type';
|
||||
import APIService from './api-service';
|
||||
|
||||
class OidcService extends APIService {
|
||||
async authorize(clientId: string, scope: string, callbackURL: string, nonce?: string) {
|
||||
async authorize(clientId: string, scope: string, callbackURL: string, nonce?: string, codeChallenge?: string, codeChallengeMethod?: string) {
|
||||
const res = await this.api.post('/oidc/authorize', {
|
||||
scope,
|
||||
nonce,
|
||||
callbackURL,
|
||||
clientId
|
||||
clientId,
|
||||
codeChallenge,
|
||||
codeChallengeMethod
|
||||
});
|
||||
|
||||
return res.data as AuthorizeResponse;
|
||||
}
|
||||
|
||||
async authorizeNewClient(clientId: string, scope: string, callbackURL: string, nonce?: string) {
|
||||
async authorizeNewClient(clientId: string, scope: string, callbackURL: string, nonce?: string, codeChallenge?: string, codeChallengeMethod?: string) {
|
||||
const res = await this.api.post('/oidc/authorize/new-client', {
|
||||
scope,
|
||||
nonce,
|
||||
callbackURL,
|
||||
clientId
|
||||
clientId,
|
||||
codeChallenge,
|
||||
codeChallengeMethod
|
||||
});
|
||||
|
||||
return res.data as AuthorizeResponse;
|
||||
|
||||
@@ -4,6 +4,7 @@ export type OidcClient = {
|
||||
logoURL: string;
|
||||
callbackURLs: [string, ...string[]];
|
||||
hasLogo: boolean;
|
||||
isPublic: boolean;
|
||||
};
|
||||
|
||||
export type OidcClientCreate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo'>;
|
||||
|
||||
@@ -12,6 +12,8 @@ export const load: PageServerLoad = async ({ url, cookies }) => {
|
||||
nonce: url.searchParams.get('nonce') || undefined,
|
||||
state: url.searchParams.get('state')!,
|
||||
callbackURL: url.searchParams.get('redirect_uri')!,
|
||||
client
|
||||
client,
|
||||
codeChallenge: url.searchParams.get('code_challenge')!,
|
||||
codeChallengeMethod: url.searchParams.get('code_challenge_method')!
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
let authorizationRequired = false;
|
||||
|
||||
export let data: PageData;
|
||||
let { scope, nonce, client, state, callbackURL } = data;
|
||||
let { scope, nonce, client, state, callbackURL, codeChallenge, codeChallengeMethod } = data;
|
||||
|
||||
async function authorize() {
|
||||
isLoading = true;
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
|
||||
await oidService
|
||||
.authorize(client!.id, scope, callbackURL, nonce)
|
||||
.authorize(client!.id, scope, callbackURL, nonce, codeChallenge, codeChallengeMethod)
|
||||
.then(async ({ code, callbackURL }) => {
|
||||
onSuccess(code, callbackURL);
|
||||
});
|
||||
@@ -55,7 +55,7 @@
|
||||
isLoading = true;
|
||||
try {
|
||||
await oidService
|
||||
.authorizeNewClient(client!.id, scope, callbackURL, nonce)
|
||||
.authorizeNewClient(client!.id, scope, callbackURL, nonce, codeChallenge, codeChallengeMethod)
|
||||
.then(async ({ code, callbackURL }) => {
|
||||
onSuccess(code, callbackURL);
|
||||
});
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
'OIDC Discovery URL': `https://${$page.url.hostname}/.well-known/openid-configuration`,
|
||||
'Token URL': `https://${$page.url.hostname}/api/oidc/token`,
|
||||
'Userinfo URL': `https://${$page.url.hostname}/api/oidc/userinfo`,
|
||||
'Certificate URL': `https://${$page.url.hostname}/.well-known/jwks.json`
|
||||
'Certificate URL': `https://${$page.url.hostname}/.well-known/jwks.json`,
|
||||
PKCE: client.isPublic ? 'Enabled' : 'Disabled'
|
||||
};
|
||||
|
||||
async function updateClient(updatedClient: OidcClientCreateWithLogo) {
|
||||
@@ -34,6 +35,8 @@
|
||||
const dataPromise = oidcService.updateClient(client.id, updatedClient);
|
||||
const imagePromise = oidcService.updateClientLogo(client, updatedClient.logo);
|
||||
|
||||
client.isPublic = updatedClient.isPublic;
|
||||
|
||||
await Promise.all([dataPromise, imagePromise])
|
||||
.then(() => {
|
||||
toast.success('OIDC client updated successfully');
|
||||
@@ -93,27 +96,29 @@
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
<div class="mb-2 mt-1 flex items-center">
|
||||
<Label class="w-44">Client secret</Label>
|
||||
{#if $clientSecretStore}
|
||||
<CopyToClipboard value={$clientSecretStore}>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-secret">
|
||||
{$clientSecretStore}
|
||||
</span>
|
||||
</CopyToClipboard>
|
||||
{:else}
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-secret"
|
||||
>••••••••••••••••••••••••••••••••</span
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
onclick={createClientSecret}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="Create new client secret"><LucideRefreshCcw class="h-3 w-3" /></Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !client.isPublic}
|
||||
<div class="mb-2 mt-1 flex items-center">
|
||||
<Label class="w-44">Client secret</Label>
|
||||
{#if $clientSecretStore}
|
||||
<CopyToClipboard value={$clientSecretStore}>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-secret">
|
||||
{$clientSecretStore}
|
||||
</span>
|
||||
</CopyToClipboard>
|
||||
{:else}
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-secret"
|
||||
>••••••••••••••••••••••••••••••••</span
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
onclick={createClientSecret}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="Create new client secret"><LucideRefreshCcw class="h-3 w-3" /></Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if showAllDetails}
|
||||
<div transition:slide>
|
||||
{#each Object.entries(setupDetails) as [key, value]}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import FileInput from '$lib/components/file-input.svelte';
|
||||
import FormInput from '$lib/components/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import type {
|
||||
OidcClient,
|
||||
@@ -28,12 +29,14 @@
|
||||
|
||||
const client: OidcClientCreate = {
|
||||
name: existingClient?.name || '',
|
||||
callbackURLs: existingClient?.callbackURLs || [""]
|
||||
callbackURLs: existingClient?.callbackURLs || [''],
|
||||
isPublic: existingClient?.isPublic || false
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
callbackURLs: z.array(z.string().url()).nonempty()
|
||||
callbackURLs: z.array(z.string().url()).nonempty(),
|
||||
isPublic: z.boolean()
|
||||
});
|
||||
|
||||
type FormSchema = typeof formSchema;
|
||||
@@ -71,15 +74,27 @@
|
||||
</script>
|
||||
|
||||
<form onsubmit={onSubmit}>
|
||||
<div class="flex flex-col gap-3 sm:flex-row">
|
||||
<div class="grid grid-cols-2 gap-3 sm:flex-row">
|
||||
<FormInput label="Name" class="w-full" bind:input={$inputs.name} />
|
||||
<OidcCallbackUrlInput
|
||||
class="w-full"
|
||||
bind:callbackURLs={$inputs.callbackURLs.value}
|
||||
bind:error={$inputs.callbackURLs.error}
|
||||
/>
|
||||
<div class="items-top flex space-x-2">
|
||||
<Checkbox id="admin-privileges" bind:checked={$inputs.isPublic.value} />
|
||||
<div class="grid gap-1.5 leading-none">
|
||||
<Label for="admin-privileges" class="mb-0 text-sm font-medium leading-none">
|
||||
Public Client
|
||||
</Label>
|
||||
<p class="text-muted-foreground text-[0.8rem]">
|
||||
Public clients do not have a client secret and use PKCE instead. Enable this if your
|
||||
client is a SPA or mobile app.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<div class="mt-8">
|
||||
<Label for="logo">Logo</Label>
|
||||
<div class="mt-2 flex items-end gap-3">
|
||||
{#if logoDataURL}
|
||||
|
||||
Reference in New Issue
Block a user