Skip to content

Commit

Permalink
Merge pull request #19 from cerberauth/create-testid-client
Browse files Browse the repository at this point in the history
feat: create testid client for testing purposes
  • Loading branch information
emmanuelgautier authored Jun 11, 2024
2 parents 832baf0 + 59e340d commit 87e5aa4
Show file tree
Hide file tree
Showing 13 changed files with 967 additions and 1,786 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Use openssl rand -base64 33 to generate a secret
AUTH_SECRET=secret

AUTH_CLIENT_ID=
AUTH_CLIENT_SECRET=
4 changes: 4 additions & 0 deletions app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { handlers } from '@/auth'

export const runtime = 'edge'
export const { GET, POST } = handlers
76 changes: 76 additions & 0 deletions app/api/testid/client/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { NextApiResponse } from 'next'
import { auth } from '@/auth'
import { GrantType, TokenEndpointAuthMethod } from '@/lib/consts'

export const runtime = 'edge'

export async function POST(req: Request, res: NextApiResponse) {
const clientData = await req.json() as OAuthClient
if (!clientData) {
return res.status(400).send('Missing client data')
}

const session = await auth()
if (!session?.token) {
return new Response('You must be logged in.', { status: 401 })
}

let method = clientData.tokenEndpointAuthMethod?.[0];
switch (method) {
case TokenEndpointAuthMethod.clientSecretPost:
method = 'client_secret_post'
case TokenEndpointAuthMethod.none:
method = 'none'
case TokenEndpointAuthMethod.clientSecretBasic:
default:
method = 'client_secret_basic'
}

const response = await fetch('https://testid.cerberauth.com/oauth2/register', {
method: 'POST',
headers: {
'Authorization': `Bearer ${session.token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
grant_types: clientData.grantTypes.map((type) => {
switch (type) {
case GrantType.authorizationCodeWithPKCE:
return 'authorization_code';
case 'refreshToken':
return 'refresh_token';
case 'clientCredentials':
return 'client_credentials';
case 'deviceCode':
return 'urn:ietf:params:oauth:grant-type:device_code';
default:
return type;
}
}),
token_endpoint_auth_method: method,

client_name: clientData.name,
allowed_cors_origins: clientData.allowedCorsOrigins,
scope: clientData.scopes.join(' '),
audience: clientData.audiences,
redirect_uris: clientData.redirectUris,
post_logout_redirect_uris: clientData.postLogoutRedirectUris,

contacts: clientData.contacts,
client_uri: clientData.uri,
policy_uri: clientData.policyUri,
tos_uri: clientData.tosUri,
logo_uri: clientData.logoUri,
}),
})
if (!response.ok) {
throw new Error(`Failed to register client: ${response.statusText}`)
}
const data = await response.json()

return Response.json({
clientId: data.client_id,
clientSecret: data.client_secret,
client: clientData,
})
}
155 changes: 138 additions & 17 deletions app/client/[client]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

export const runtime = 'edge'

import { useEffect, useState } from 'react'
import { useParams } from 'next/navigation'
import { useCallback, useEffect, useState } from 'react'
import { useSession, signIn } from 'next-auth/react'
import { usePlausible } from 'next-plausible'
import Link from 'next/link'
import { useParams, useSearchParams } from 'next/navigation'

import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { urlDecode } from '@/lib/url'
Expand All @@ -15,41 +18,159 @@ import { ApplicationType, GrantType, TokenEndpointAuthMethod } from '@/lib/const
export const dynamic = 'force-static'
export const dynamicParams = false

const testIdOIDCDiscoveryEndpoint = 'https://testid.cerberauth.com/.well-known/openid-configuration'
const localStorageItem = (id: string) => `testidClient:${id}`

const createShareableLink = (medium: string) => {
const url = new URL(window.location.href)
url.searchParams.set('utm_source', 'cerberauth')
url.searchParams.set('utm_medium', medium)
return url.toString()
}

const createClient = async (client: OAuthClient) => {
const response = await fetch('/api/testid/client', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(client),
})
if (!response.ok) {
throw new Error(`Failed to create client: ${response.statusText}`)
}
return response.json()
}

export default function ClientPage() {
const searchParams = useSearchParams()
const session = useSession()
const plausible = usePlausible()
const [client, setClient] = useState<OAuthClient | null>(null)
const { client: clientEncodedParam } = useParams<{ client: string }>()
const [testIdClient, setTestIdClient] = useState<TestIdClient | null>(null)

const createTestIdClient = useCallback(async () => {
if (!client) {
return
}

plausible('createTestIdClient')
if (session.status === 'unauthenticated') {
const callbackUrl = new URL(window.location.href)
callbackUrl.searchParams.set('test_id_client', 'created')

signIn('cerberauth', { callbackUrl: callbackUrl.toString() });
return;
}

const newTestIdClient = await createClient(client)
plausible('testIdClientCreated')
setTestIdClient(newTestIdClient)
localStorage.setItem(localStorageItem(client.id || client.name), JSON.stringify(newTestIdClient))
}, [client, session, plausible])

useEffect(() => {
urlDecode(clientEncodedParam).then((data) => setClient(data))
}, [clientEncodedParam])

useEffect(() => {
if (!client) {
return
}

const item = localStorage.getItem(localStorageItem(client.id || client.name))
if (item) {
setTestIdClient(JSON.parse(item))
}
}, [client])

useEffect(() => {
if (!client || !searchParams.has('test_id_client') || testIdClient) {
return
}

createTestIdClient()
}, [client, testIdClient, searchParams, createTestIdClient])

if (!client) {
return <div>Loading...</div>
}

const onClipboardCopy = () => {
const url = new URL(window.location.href)
url.searchParams.set('client', clientEncodedParam)
plausible('clientUrlClipboardCopy')
const url = createShareableLink('clipboard')
navigator.clipboard.writeText(url.toString())
}

const shareByEmail = () => {
const url = new URL(window.location.href)
url.searchParams.set('client', clientEncodedParam)
const message = `mailto:?subject=${encodeURIComponent('New Client Request')}&body=${encodeURIComponent(`Please we would need you to create a new OAuth2 client. You can check the following link for all the client details: ${window.location.href}`)}`
plausible('clientShareByEmail')
const url = createShareableLink('email')
const message = `mailto:?subject=${encodeURIComponent('New Client Request')}&body=${encodeURIComponent(`Please we would need you to create a new OAuth2 client. You can check the following link for all the client details: ${url}`)}`
window.open(message)
console.log(message)
}

return (
<main className="container mx-auto max-w-4xl px-4 py-12 space-y-8">
<div>
<h1 className="text-3xl font-semibold leading-none tracking-tight mb-2">Client Details</h1>
<p className="text-sm text-muted-foreground">
Check one last time before you submit it for review and creation.
</p>
<h1 className="text-3xl font-semibold leading-none tracking-tight mb-2 text-center">{client.name} Client</h1>
</div>

{testIdClient ? (
<Card>
<CardHeader title="Test Client">
<CardTitle>Temporary Client</CardTitle>
</CardHeader>

<CardContent>
<p className="text-muted-foreground">
A temporary client has been created for you to perform tests. This client will be available for a limited time and will be deleted automatically.
</p>

<Separator className="my-4" />

<ul className="grid gap-3">
<li className="flex items-center justify-between">
<span className="text-muted-foreground">
OpenID Connect Configuration
</span>
<Link href={testIdOIDCDiscoveryEndpoint} target='_blank'>{testIdOIDCDiscoveryEndpoint}</Link>
</li>

<li className="flex items-center justify-between">
<span className="text-muted-foreground">
Client ID
</span>
<span>{testIdClient.clientId}</span>
</li>

{testIdClient.clientSecret && (
<li className="flex items-center justify-between">
<span className="text-muted-foreground">
Client Secret
</span>
<span>{testIdClient.clientSecret}</span>
</li>
)}
</ul>
</CardContent>
</Card>
) : (
<Card>
<CardHeader title="Test Client">
<CardTitle>Create a temporary Client</CardTitle>
</CardHeader>

<CardContent>
<p className="text-muted-foreground">
This will allow you to test the integration in your application while waiting for your real client to be created.
</p>
</CardContent>

<CardFooter className="flex justify-end">
<Button onClick={createTestIdClient}>Create a Client</Button>
</CardFooter>
</Card>
)}

<Card>
<CardHeader title="Client Information">
<CardTitle>{client.name} Client</CardTitle>
Expand Down Expand Up @@ -138,7 +259,7 @@ export default function ClientPage() {
{Array.isArray(client.postLogoutRedirectUris) && client.postLogoutRedirectUris.length > 0 && (
<li className="flex justify-between">
<span className="text-muted-foreground">
Front Channel Logout URI
Post Logout Redirect URIs
</span>
<ul className="text-right space-y-2">
{client.postLogoutRedirectUris.map(uri => (
Expand All @@ -158,7 +279,7 @@ export default function ClientPage() {
<span className="text-muted-foreground">
URI
</span>
<span>{client.uri}</span>
<Link href={client.uri} target="_blank" rel="nofollow">{client.uri}</Link>
</li>
)}

Expand All @@ -176,7 +297,7 @@ export default function ClientPage() {
<span className="text-muted-foreground">
Policy URI
</span>
<span>{client.policyUri}</span>
<Link href={client.policyUri} target="_blank" rel="nofollow">{client.policyUri}</Link>
</li>
)}

Expand All @@ -185,7 +306,7 @@ export default function ClientPage() {
<span className="text-muted-foreground">
Terms of Service URI
</span>
<span>{client.tosUri}</span>
<Link href={client.tosUri} target="_blank" rel="nofollow">{client.tosUri}</Link>
</li>
)}
</ul>
Expand All @@ -196,6 +317,6 @@ export default function ClientPage() {
<Button onClick={shareByEmail}>Share by Email</Button>
</CardFooter>
</Card>
</main >
</main>
)
}
10 changes: 6 additions & 4 deletions app/client/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { zodResolver } from '@hookform/resolvers/zod'
import { CircleHelp } from 'lucide-react'
import { nanoid } from 'nanoid'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
Expand All @@ -18,7 +19,7 @@ import { ApplicationType, GrantType, TokenEndpointAuthMethod } from '@/lib/const
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import { urlEncode } from '@/lib/url'

const sessionStorageItem = 'client'
const localStorageItem = 'client'

const createClientSchema = z.object({
applicationType: z.enum(Object.keys(ApplicationType) as [ApplicationType]),
Expand Down Expand Up @@ -52,7 +53,7 @@ export default function CreateClient() {
return
}

const client = sessionStorage.getItem(sessionStorageItem)
const client = localStorage.getItem(localStorageItem)
form.reset(client ? JSON.parse(client) : undefined)
setHasBeenInitialized(true)
}, [hasBeenInitialized, form])
Expand All @@ -62,7 +63,7 @@ export default function CreateClient() {
return
}

sessionStorage.setItem(sessionStorageItem, JSON.stringify(data))
localStorage.setItem(localStorageItem, JSON.stringify(data))
}, [hasBeenInitialized, data])

function onApplicationTypeChange(type: ApplicationType | null) {
Expand All @@ -85,6 +86,7 @@ export default function CreateClient() {
function onSubmit(data: z.infer<typeof createClientSchema>) {
const client: OAuthClient = {
...data,
id: nanoid(),
audiences: data.audiences || [],
scopes: data.scopes || [],
allowedCorsOrigins: data.allowedCorsOrigins || [],
Expand All @@ -96,7 +98,7 @@ export default function CreateClient() {

urlEncode(client).then(encoded => {
router.push(`/client/${encoded}`)
sessionStorage.removeItem(sessionStorageItem)
localStorage.removeItem(localStorageItem)
})
}

Expand Down
Loading

0 comments on commit 87e5aa4

Please sign in to comment.