diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 5326d5e89..e93532d63 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -34,7 +34,7 @@ Cypress.Commands.add('createOIDCIntegration', (organizationName: string) => { cy.get('input[id="clientId"]').type('implicit-mock-client'); cy.get('input[id="clientSecret"]').type('client-credentials-mock-client-secret'); - cy.get('div[role="dialog"]').find('button[type="submit"]').click(); + cy.get('div[role="dialog"]').find('button[type="submit"]').last().click(); cy.url().then(url => { return new URL(url).pathname.split('/')[0]; diff --git a/packages/web/app/src/components/organization/settings/oidc-integration-section.tsx b/packages/web/app/src/components/organization/settings/oidc-integration-section.tsx index 9ae97d8a6..e7d787e9f 100644 --- a/packages/web/app/src/components/organization/settings/oidc-integration-section.tsx +++ b/packages/web/app/src/components/organization/settings/oidc-integration-section.tsx @@ -1,8 +1,10 @@ import { ReactElement, useEffect, useRef } from 'react'; import { format } from 'date-fns'; import { useFormik } from 'formik'; +import { useForm } from 'react-hook-form'; import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'; import { useClient, useMutation } from 'urql'; +import { z } from 'zod'; import { Button, buttonVariants } from '@/components/ui/button'; import { Dialog, @@ -12,6 +14,14 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form'; import { AlertTriangleIcon, KeyIcon } from '@/components/ui/icon'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -24,6 +34,8 @@ import { DocumentType, FragmentType, graphql, useFragment } from '@/gql'; import { useClipboard } from '@/lib/hooks'; import { useResetState } from '@/lib/hooks/use-reset-state'; import { cn } from '@/lib/utils'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation as useRQMutation } from '@tanstack/react-query'; import { Link, useRouter } from '@tanstack/react-router'; function CopyInput(props: { value: string; id?: string }) { @@ -227,6 +239,165 @@ function CreateOIDCIntegrationModal(props: { ); } +const OIDCMetadataSchema = z.object({ + token_endpoint: z + .string({ + required_error: 'Token endpoint not found', + }) + .url('Token endpoint must be a valid URL'), + userinfo_endpoint: z + .string({ + required_error: 'Userinfo endpoint not found', + }) + .url('Userinfo endpoint must be a valid URL'), + authorization_endpoint: z + .string({ + required_error: 'Authorization endpoint not found', + }) + .url('Authorization endpoint must be a valid URL'), +}); + +async function fetchOIDCMetadata(url: string) { + const res = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }); + + if (!res.ok) { + return { + ok: false, + error: { + message: 'Failed to fetch metadata', + details: { + url, + status: res.status, + statusText: res.statusText, + body: await res.text(), + }, + }, + } as const; + } + + return { + ok: true, + metadata: await res.json(), + } as const; +} + +const OIDCMetadataFormSchema = z.object({ + url: z.string().url('Must be a valid URL'), +}); + +function OIDCMetadataFetcher(props: { + onEndpointChange(endpoints: { token: string; userinfo: string; authorization: string }): void; +}) { + const { toast } = useToast(); + + const fetchMetadata = useRQMutation({ + mutationFn: fetchOIDCMetadata, + onSuccess(data) { + if (!data.ok) { + toast({ + title: data.error.message, + description: ( +
Status: {data.error.details.status}
+Response: {data.error.details.body ?? data.error.details.statusText}
+{msg}
+ ))} + > + ), + variant: 'destructive', + }); + return; + } + + props.onEndpointChange({ + token: metadataResult.data.token_endpoint, + userinfo: metadataResult.data.userinfo_endpoint, + authorization: metadataResult.data.authorization_endpoint, + }); + }, + onError(error) { + console.error(error); + toast({ + title: 'Failed to fetch OIDC metadata', + description: 'Provide the endpoints manually or try again later', + variant: 'destructive', + }); + }, + }); + + function onSubmit(data: z.infer