From 92b92e13b56b78e5e778d7a32805eb79d449d9e4 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Tue, 14 Jan 2025 19:04:13 +0100 Subject: [PATCH 01/58] NORY-46: add button to create client --- dashboard/src/app/(inside)/client/create/page.tsx | 12 ++++++++++++ dashboard/src/app/(inside)/client/page.tsx | 9 ++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 dashboard/src/app/(inside)/client/create/page.tsx diff --git a/dashboard/src/app/(inside)/client/create/page.tsx b/dashboard/src/app/(inside)/client/create/page.tsx new file mode 100644 index 0000000..8b67099 --- /dev/null +++ b/dashboard/src/app/(inside)/client/create/page.tsx @@ -0,0 +1,12 @@ +export default async function CreateClientPage() { + return ( +
+
+

Create OAuth2 Client

+

+ Configure your new OAuth2 Client. +

+
+
+ ); +} \ No newline at end of file diff --git a/dashboard/src/app/(inside)/client/page.tsx b/dashboard/src/app/(inside)/client/page.tsx index 0d3f5a3..be59eba 100644 --- a/dashboard/src/app/(inside)/client/page.tsx +++ b/dashboard/src/app/(inside)/client/page.tsx @@ -1,5 +1,7 @@ import { getOAuth2Api } from '@/ory/sdk/server'; import { ClientDataTable } from '@/app/(inside)/client/data-table'; +import { Button } from '@/components/ui/button'; +import Link from 'next/link'; export interface FetchClientPageProps { pageSize: number; @@ -50,11 +52,16 @@ export default async function ListClientPage() { return (
-
+

OAuth2 Clients

See and manage all OAuth2 clients registered with your Ory Hydra instance

+
Date: Sun, 26 Jan 2025 20:33:21 +0100 Subject: [PATCH 02/58] NORY-46: add basic action to create client --- dashboard/src/lib/action/client.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 dashboard/src/lib/action/client.ts diff --git a/dashboard/src/lib/action/client.ts b/dashboard/src/lib/action/client.ts new file mode 100644 index 0000000..5cc2799 --- /dev/null +++ b/dashboard/src/lib/action/client.ts @@ -0,0 +1,29 @@ +'use server'; + +import { clientFormSchema } from '@/lib/forms/client-form'; +import { z } from 'zod'; +import { getFrontendApi, getOAuth2Api } from '@/ory/sdk/server'; +import { cookies } from 'next/headers'; + +export async function createClient( + formData: z.infer, +) { + + const cookie = await cookies(); + const frontendApi = await getFrontendApi(); + + const session = await frontendApi + .toSession({ cookie: 'ory_kratos_session=' + cookie.get('ory_kratos_session')?.value }) + .then((response) => response.data) + .catch(() => null); + + if (!session) { + console.log('Unauthorised action call'); + throw 'Unauthorised'; + } + + console.log(session.identity?.traits.email, 'posted form', formData); + + const oauthApi = await getOAuth2Api(); + return await oauthApi.createOAuth2Client({ oAuth2Client: formData }); +} From 253ad4e2b027451f647646c3211446619121ec24 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Tue, 18 Feb 2025 09:12:49 +0100 Subject: [PATCH 03/58] NORY-46: modify card component for mobile devices --- dashboard/src/components/ui/card.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dashboard/src/components/ui/card.tsx b/dashboard/src/components/ui/card.tsx index 3c69472..22ccc98 100644 --- a/dashboard/src/components/ui/card.tsx +++ b/dashboard/src/components/ui/card.tsx @@ -23,7 +23,7 @@ const CardHeader = React.forwardRef< >(({ className, ...props }, ref) => (
)); @@ -60,7 +60,7 @@ const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -
+
)); CardContent.displayName = 'CardContent'; @@ -70,7 +70,7 @@ const CardFooter = React.forwardRef< >(({ className, ...props }, ref) => (
)); From 749974b7ec55947c6ff4dc9727127e150816fe64 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Tue, 18 Feb 2025 09:13:27 +0100 Subject: [PATCH 04/58] NORY-46: add initial create-client form to page --- .../src/app/(inside)/client/create/page.tsx | 4 + .../src/components/forms/client-form.tsx | 276 ++++++++++++++++++ dashboard/src/lib/forms/client-form.ts | 12 + 3 files changed, 292 insertions(+) create mode 100644 dashboard/src/components/forms/client-form.tsx create mode 100644 dashboard/src/lib/forms/client-form.ts diff --git a/dashboard/src/app/(inside)/client/create/page.tsx b/dashboard/src/app/(inside)/client/create/page.tsx index 8b67099..f42fda6 100644 --- a/dashboard/src/app/(inside)/client/create/page.tsx +++ b/dashboard/src/app/(inside)/client/create/page.tsx @@ -1,3 +1,6 @@ +import { CreateClientForm } from '@/components/forms/client-form'; +import { createClient } from '@/lib/action/client'; + export default async function CreateClientPage() { return (
@@ -7,6 +10,7 @@ export default async function CreateClientPage() { Configure your new OAuth2 Client.

+
); } \ No newline at end of file diff --git a/dashboard/src/components/forms/client-form.tsx b/dashboard/src/components/forms/client-form.tsx new file mode 100644 index 0000000..afaca44 --- /dev/null +++ b/dashboard/src/components/forms/client-form.tsx @@ -0,0 +1,276 @@ +'use client'; + +import { z } from 'zod'; +import { clientFormSchema } from '@/lib/forms/client-form'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { kratos } from '@/ory'; +import { useEffect, useState } from 'react'; +import { Identity, OAuth2Client } from '@ory/client'; +import { AxiosResponse } from 'axios'; +import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { useRouter } from 'next/navigation'; +import { Checkbox } from '@/components/ui/checkbox'; + +interface CreateClientFormProps { + action: (data: z.infer) => Promise>; +} + +export function CreateClientForm({ action }: CreateClientFormProps) { + + const router = useRouter(); + const [identity, setIdentity] = useState(); + + useEffect(() => { + kratos.toSession() + .then(response => response.data) + .then(session => setIdentity(session.identity)); + }, []); + + const form = useForm>({ + resolver: zodResolver(clientFormSchema), + defaultValues: { + client_name: '', + owner: '', + scope: '', + skip: false, + logo_uri: '', + policy_uri: '', + tos_uri: '', + }, + }); + + const [successDialogOpen, setSuccessDialogOpen] = useState(false); + const [createdClient, setCreatedClient] = useState(); + const handleSubmit = async (data: z.infer) => { + await action(data) + .then((response) => { + console.log(response); + return response.data; + }) + .then((client) => { + setCreatedClient(client); + setSuccessDialogOpen(true); + }) + .catch((error) => { + console.error(error); + }); + }; + + useEffect(() => { + form.setValue('owner', identity?.id ?? ''); + }, [identity]); + + return ( + <> + { + createdClient && ( + setSuccessDialogOpen(false)}> + + + Client created + + Your client was created successfully. Make sure to safe the client secret! + + + + ) + } +
+ + + + + + Essentials + + + + ( + + Client Name + + + + + The human-readable name of the client to be presented to the end-user + during + authorization. + + + + )} + /> + ( + + Scopes + + + + + Scope is a string containing a space-separated list of scope values (as + described in + Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when + requesting + access + tokens. + + + + )} + /> + ( + + Scopes + + + + + Scope is a string containing a space-separated list of scope values (as + described in + Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when + requesting + access + tokens. + + + + )} + /> + + + + + + + Consent Screen + + + + ( + +
+ + Skip consent + + + Whether or not the consent screen is skipped for this client + +
+ + + +
+ )}/> + { + !form.getValues('skip') && ( + <> + ( + + Logo URI + + + + + A URL string referencing the client's logo. + + + + )}/> + + ( + + Policy URI + + + + + A URL string pointing to a human-readable + privacy policy document for the client that describes how the + deployment organization collects, uses, retains, and discloses + personal data. + + + + )}/> + + ( + + Terms URI + + + + + A URL string pointing to a human-readable terms of service + document for the client that describes a contractual + relationship between the end-user and the client that the + end-user accepts when authorizing the client. + + + + )}/> + + ( + + Owner + + + + + Owner is a string identifying the owner of the OAuth 2.0 Client. + + + + )} + /> + + ) + } +
+
+ +
+ + +
+
+ + + ); +} diff --git a/dashboard/src/lib/forms/client-form.ts b/dashboard/src/lib/forms/client-form.ts new file mode 100644 index 0000000..b11378e --- /dev/null +++ b/dashboard/src/lib/forms/client-form.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +export const clientFormSchema = z.object({ + access_token_strategy: z.string().default('opaque').readonly(), + client_name: z.string().min(1, 'Client name is required'), + owner: z.string().min(1, 'Owner is required'), + scope: z.string(), + skip: z.boolean(), + logo_uri: z.string().url(), + tos_uri: z.string().url(), + policy_uri: z.string().url(), +}); From edbb93c03bc1e2a77187bf2f776664663c45c73d Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Tue, 18 Feb 2025 09:29:02 +0100 Subject: [PATCH 05/58] NORY-46: remove owner autofill --- .../src/components/forms/client-form.tsx | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/dashboard/src/components/forms/client-form.tsx b/dashboard/src/components/forms/client-form.tsx index afaca44..b4191d9 100644 --- a/dashboard/src/components/forms/client-form.tsx +++ b/dashboard/src/components/forms/client-form.tsx @@ -7,12 +7,11 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import { kratos } from '@/ory'; -import { useEffect, useState } from 'react'; -import { Identity, OAuth2Client } from '@ory/client'; +import { useState } from 'react'; +import { OAuth2Client } from '@ory/client'; import { AxiosResponse } from 'axios'; import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { useRouter } from 'next/navigation'; import { Checkbox } from '@/components/ui/checkbox'; @@ -23,13 +22,6 @@ interface CreateClientFormProps { export function CreateClientForm({ action }: CreateClientFormProps) { const router = useRouter(); - const [identity, setIdentity] = useState(); - - useEffect(() => { - kratos.toSession() - .then(response => response.data) - .then(session => setIdentity(session.identity)); - }, []); const form = useForm>({ resolver: zodResolver(clientFormSchema), @@ -61,10 +53,6 @@ export function CreateClientForm({ action }: CreateClientFormProps) { }); }; - useEffect(() => { - form.setValue('owner', identity?.id ?? ''); - }, [identity]); - return ( <> { @@ -180,7 +168,8 @@ export function CreateClientForm({ action }: CreateClientFormProps) { /> - )}/> + )} + /> { !form.getValues('skip') && ( <> @@ -199,7 +188,8 @@ export function CreateClientForm({ action }: CreateClientFormProps) { - )}/> + )} + /> - )}/> + )} + /> - )}/> + )} + /> Owner - + Owner is a string identifying the owner of the OAuth 2.0 Client. From ce28973deafda9e914d06af38d127faf01d31535 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Tue, 18 Feb 2025 10:14:33 +0100 Subject: [PATCH 06/58] NORY-46: add dynamic redirect_uri input --- .../src/components/forms/client-form.tsx | 88 +++++++++++++++---- dashboard/src/lib/forms/client-form.ts | 3 +- 2 files changed, 72 insertions(+), 19 deletions(-) diff --git a/dashboard/src/components/forms/client-form.tsx b/dashboard/src/components/forms/client-form.tsx index b4191d9..88a34b7 100644 --- a/dashboard/src/components/forms/client-form.tsx +++ b/dashboard/src/components/forms/client-form.tsx @@ -14,6 +14,7 @@ import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle } import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { useRouter } from 'next/navigation'; import { Checkbox } from '@/components/ui/checkbox'; +import { Minus } from 'lucide-react'; interface CreateClientFormProps { action: (data: z.infer) => Promise>; @@ -22,17 +23,19 @@ interface CreateClientFormProps { export function CreateClientForm({ action }: CreateClientFormProps) { const router = useRouter(); + const [redirectUris, setRedirectUris] = useState(['']); const form = useForm>({ resolver: zodResolver(clientFormSchema), defaultValues: { client_name: '', - owner: '', scope: '', + redirect_uris: [''], skip: false, logo_uri: '', policy_uri: '', tos_uri: '', + owner: '', }, }); @@ -53,6 +56,23 @@ export function CreateClientForm({ action }: CreateClientFormProps) { }); }; + const addRedirectUri = () => { + setRedirectUris([...redirectUris, '']); + }; + + const removeRedirectUri = (index) => { + const updatedRedirectUris = redirectUris.filter((_, i) => i !== index); + setRedirectUris(updatedRedirectUris); + form.setValue('redirect_uris', updatedRedirectUris); + }; + + const handleInputChange = (index, event) => { + const updatedRedirectUris = [...redirectUris]; + updatedRedirectUris[index] = event.target.value; + setRedirectUris(updatedRedirectUris); + form.setValue('redirect_uris', updatedRedirectUris); + }; + return ( <> { @@ -117,27 +137,44 @@ export function CreateClientForm({ action }: CreateClientFormProps) { )} /> - ( + {redirectUris.map((uri, index) => ( +
- Scopes + Redirect + URI {index + 1} - +
+ handleInputChange(index, event)} + /> + {redirectUris.length > 1 && ( + + )} +
- - Scope is a string containing a space-separated list of scope values (as - described in - Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when - requesting - access - tokens. - - + {form.errors?.redirect_uris && form.errors.redirect_uris[index] && ( + {form.errors.redirect_uris[index].message} + )}
- )} - /> +
+ ))} + + @@ -162,6 +199,7 @@ export function CreateClientForm({ action }: CreateClientFormProps) {
+ {/* TODO: change to switch */} + + + + Supported OAuth2 flows + + + Configure allowed grant types and response types for this OAuth2 Client. + + + + + + +
+ + ); + })} + {/* Avoid having the "Search" Icon */} + setOpen(false)} + onFocus={() => setOpen(true)} + placeholder={placeholder ?? 'Select an item...'} + className="ml-2 flex-1 bg-transparent outline-none placeholder:text-muted-foreground" + /> +
+
+
+ + {open && selectables.length > 0 ? ( +
+ + {selectables.map((item) => { + return ( + { + e.preventDefault(); + e.stopPropagation(); + }} + onSelect={(value) => { + setInputValue(''); + setSelected((prev) => [...prev, item]); + }} + className={'cursor-pointer'} + > + {item.label} + + ); + })} + +
+ ) : null} +
+
+ + ); +} \ No newline at end of file diff --git a/dashboard/src/components/ui/select.tsx b/dashboard/src/components/ui/select.tsx new file mode 100644 index 0000000..ddd77ac --- /dev/null +++ b/dashboard/src/components/ui/select.tsx @@ -0,0 +1,160 @@ +'use client'; + +import * as React from 'react'; +import * as SelectPrimitive from '@radix-ui/react-select'; +import { Check, ChevronDown, ChevronUp } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1', + className, + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = 'popper', ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; From b70afcf16ce2c567f422438a2d0bcaeb23bd357a Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Tue, 25 Feb 2025 15:58:53 +0100 Subject: [PATCH 09/58] NORY-46: add client auth mechanism --- .../src/components/forms/client-form.tsx | 53 +++++++++++++++++-- dashboard/src/lib/forms/client-form.ts | 1 + 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/dashboard/src/components/forms/client-form.tsx b/dashboard/src/components/forms/client-form.tsx index 054023e..84ceebf 100644 --- a/dashboard/src/components/forms/client-form.tsx +++ b/dashboard/src/components/forms/client-form.tsx @@ -15,6 +15,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { useRouter } from 'next/navigation'; import { Checkbox } from '@/components/ui/checkbox'; import { Minus } from 'lucide-react'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; interface CreateClientFormProps { action: (data: z.infer) => Promise>; @@ -60,13 +61,13 @@ export function CreateClientForm({ action }: CreateClientFormProps) { setRedirectUris([...redirectUris, '']); }; - const removeRedirectUri = (index) => { + const removeRedirectUri = (index: number) => { const updatedRedirectUris = redirectUris.filter((_, i) => i !== index); setRedirectUris(updatedRedirectUris); form.setValue('redirect_uris', updatedRedirectUris); }; - const handleInputChange = (index, event) => { + const handleInputChange = (index: number, event: any) => { const updatedRedirectUris = [...redirectUris]; updatedRedirectUris[index] = event.target.value; setRedirectUris(updatedRedirectUris); @@ -165,8 +166,8 @@ export function CreateClientForm({ action }: CreateClientFormProps) { )}
- {form.errors?.redirect_uris && form.errors.redirect_uris[index] && ( - {form.errors.redirect_uris[index].message} + {form.formState.errors?.redirect_uris && form.formState.errors.redirect_uris[index] && ( + {form.formState.errors.redirect_uris[index].message} )}
@@ -341,6 +342,50 @@ export function CreateClientForm({ action }: CreateClientFormProps) { + + + + Client authentication mechanism + + + Set the client authentication method for the token endpoint. By default the client + credentials must be sent in the body of an HTTP POST. This option can also specify for + sending the credentials encoded in the HTTP Authorization header or by using JSON Web + Tokens. Specify none for public clients (native apps, mobile apps) which can not have + secrets. + + + + ( + + Authentication method + + + + )} + /> + + +
- {/* TODO: change to switch */} - @@ -238,7 +238,9 @@ export function CreateClientForm({ action }: CreateClientFormProps) { Policy URI + placeholder="https://myapp.example/privacy_policy" + {...field} + /> A URL string pointing to a human-readable diff --git a/dashboard/src/components/ui/switch.tsx b/dashboard/src/components/ui/switch.tsx new file mode 100644 index 0000000..4c01734 --- /dev/null +++ b/dashboard/src/components/ui/switch.tsx @@ -0,0 +1,29 @@ +'use client'; + +import * as React from 'react'; +import * as SwitchPrimitives from '@radix-ui/react-switch'; + +import { cn } from '@/lib/utils'; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; From 8c1e38efda272f443376ed412d8fcba44f047d30 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Tue, 25 Feb 2025 17:15:23 +0100 Subject: [PATCH 11/58] NORY-46: move form description above input --- .../src/components/forms/client-form.tsx | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/dashboard/src/components/forms/client-form.tsx b/dashboard/src/components/forms/client-form.tsx index 76ff0c8..4ef7693 100644 --- a/dashboard/src/components/forms/client-form.tsx +++ b/dashboard/src/components/forms/client-form.tsx @@ -106,14 +106,13 @@ export function CreateClientForm({ action }: CreateClientFormProps) { render={({ field }) => ( Client Name + + The human-readable name of the client to be presented to the end-user during + authorization. + - - The human-readable name of the client to be presented to the end-user - during - authorization. - )} @@ -124,17 +123,14 @@ export function CreateClientForm({ action }: CreateClientFormProps) { render={({ field }) => ( Scopes + + Scope is a string containing a space-separated list of scope values (as + described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use + when requesting access tokens. + - - Scope is a string containing a space-separated list of scope values (as - described in - Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when - requesting - access - tokens. - )} @@ -218,13 +214,13 @@ export function CreateClientForm({ action }: CreateClientFormProps) { render={({ field }) => ( Logo URI + + A URL string referencing the client's logo. + - - A URL string referencing the client's logo. - )} @@ -236,18 +232,18 @@ export function CreateClientForm({ action }: CreateClientFormProps) { render={({ field }) => ( Policy URI + + A URL string pointing to a human-readable privacy policy + document + for the client that describes how the deployment organization + collects, uses, retains, and discloses personal data. + - - A URL string pointing to a human-readable - privacy policy document for the client that describes how the - deployment organization collects, uses, retains, and discloses - personal data. - )} @@ -259,16 +255,16 @@ export function CreateClientForm({ action }: CreateClientFormProps) { render={({ field }) => ( Terms URI - - - A URL string pointing to a human-readable terms of service document for the client that describes a contractual relationship between the end-user and the client that the end-user accepts when authorizing the client. + + + )} @@ -280,12 +276,12 @@ export function CreateClientForm({ action }: CreateClientFormProps) { render={({ field }) => ( Owner - - - Owner is a string identifying the owner of the OAuth 2.0 Client. + + + )} From 2bdf7c5c4e5ea88f1da85bc71a4a6f2d9ff08f6f Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Tue, 25 Feb 2025 17:23:14 +0100 Subject: [PATCH 12/58] NORY-46: add 'OpenID Connect logout' section --- .../src/components/forms/client-form.tsx | 191 ++++++++++++++++++ dashboard/src/lib/forms/client-form.ts | 7 +- 2 files changed, 197 insertions(+), 1 deletion(-) diff --git a/dashboard/src/components/forms/client-form.tsx b/dashboard/src/components/forms/client-form.tsx index 4ef7693..fc5d6be 100644 --- a/dashboard/src/components/forms/client-form.tsx +++ b/dashboard/src/components/forms/client-form.tsx @@ -25,7 +25,9 @@ interface CreateClientFormProps { export function CreateClientForm({ action }: CreateClientFormProps) { const router = useRouter(); + const [redirectUris, setRedirectUris] = useState(['']); + const [postLogoutRedirectUris, setPostLogoutRedirectUris] = useState(['']); const form = useForm>({ resolver: zodResolver(clientFormSchema), @@ -62,12 +64,22 @@ export function CreateClientForm({ action }: CreateClientFormProps) { setRedirectUris([...redirectUris, '']); }; + const addPostLogoutRedirectUri = () => { + setPostLogoutRedirectUris([...postLogoutRedirectUris, '']); + }; + const removeRedirectUri = (index: number) => { const updatedRedirectUris = redirectUris.filter((_, i) => i !== index); setRedirectUris(updatedRedirectUris); form.setValue('redirect_uris', updatedRedirectUris); }; + const removePostLogoutRedirectUri = (index: number) => { + const updatedPostLogoutRedirectUris = postLogoutRedirectUris.filter((_, i) => i !== index); + setPostLogoutRedirectUris(postLogoutRedirectUris); + form.setValue('post_logout_redirect_uris', updatedPostLogoutRedirectUris); + }; + const handleInputChange = (index: number, event: any) => { const updatedRedirectUris = [...redirectUris]; updatedRedirectUris[index] = event.target.value; @@ -75,6 +87,13 @@ export function CreateClientForm({ action }: CreateClientFormProps) { form.setValue('redirect_uris', updatedRedirectUris); }; + const handlePostLogoutInputChange = (index: number, event: any) => { + const updatedPostLogoutRedirectUris = [...postLogoutRedirectUris]; + updatedPostLogoutRedirectUris[index] = event.target.value; + setPostLogoutRedirectUris(updatedPostLogoutRedirectUris); + form.setValue('post_logout_redirect_uris', updatedPostLogoutRedirectUris); + }; + return ( <> { @@ -384,6 +403,178 @@ export function CreateClientForm({ action }: CreateClientFormProps) { + + + + OpenID Connect logout + + + Get more information about using front and backchannels here  + . + + + + ( + +
+ + Frontchannel Logout Session Required + + + Boolean value specifying whether the Relay Party (RP) requires that + issuer and session ID query parameters be included to identify the RP + session with the OpenID provider (OP) when the Frontchannel Logout URI + is used. The default value is false. + +
+ + + +
+ )} + /> + ( + + Frontchannel Logout URI + + URL that will cause the Relying Party (RP) to log itself out when rendered + in an iframe by the OpenID provider (OP). An issuer query parameter and a + session ID query parameter MAY be included by the OpenID provider (OP) to + enable the Relying Party (RP) to validate the request and to determine which + of the potentially multiple sessions is to be logged out; if either is + included, both MUST be. + + + + + + + )} + /> + ( + +
+ + Backchannel Logout Session Required + + + Boolean value specifying whether the Relying Party (RP) requires that a + session ID Claim be included in the Logout Token to identify the Relying + Party session with the OpenID provider (OP) when the Backchannel Logout + URI is used. If omitted, the default value is false. + +
+ + + +
+ )} + /> + ( + + Backchannel Logout URI + + URL that will cause the Relying Party (RP) to log itself out when rendered + in an iframe by the OpenID provider (OP). An issuer query parameter and a + session ID query parameter MAY be included by the OpenID provider (OP) to + enable the Relying Party (RP) to validate the request and to determine which + of the potentially multiple sessions is to be logged out; if either is + included, both MUST be. + + + + + + + )} + /> + ( + +
+ + Skip logout consent + + + Boolean value specifying whether the additional logout consent screen + should be skipped. + +
+ + + +
+ )} + /> + {postLogoutRedirectUris.map((uri, index) => ( +
+ + + Post Logout Redirect URI {index + 1} + +
+ handlePostLogoutInputChange(index, event)} + /> + {postLogoutRedirectUris.length > 1 && ( + + )} +
+
+ {form.formState.errors?.post_logout_redirect_uris && form.formState.errors.post_logout_redirect_uris[index] && ( + {form.formState.errors.post_logout_redirect_uris[index].message} + )} +
+
+ ))} + + +
+
+