diff --git a/app/login/vercel/actions.ts b/app/login/vercel/actions.ts index d92e06d..008bac7 100644 --- a/app/login/vercel/actions.ts +++ b/app/login/vercel/actions.ts @@ -50,3 +50,24 @@ export async function startAuthorization(formData: FormData) { cookies.set("vercel-oidc-state", state, { httpOnly: true }); redirect(redirectTo); } + +export async function startImplicitAuthorization(formData: FormData) { + console.log("startImplicitAuthorization:", Object.fromEntries(formData)); + const headers = getHeaders(); + const cookies = getCookies(); + + const host = headers.get("host"); + + const protocol = host?.startsWith("localhost") ? "http" : "https"; + const callbackUrl = `${protocol}://${host}/login/vercel/callback-implicit`; + const { redirectTo, state } = await createAuthorizationUrl({ + callbackUrl, + explicit: false, + }); + console.log("Redirecting to authorization URL:", { + redirectTo, + state, + }); + cookies.set("vercel-oidc-state", state, { httpOnly: true }); + redirect(redirectTo); +} diff --git a/app/login/vercel/callback-implicit/actions.ts b/app/login/vercel/callback-implicit/actions.ts new file mode 100644 index 0000000..aaecac6 --- /dev/null +++ b/app/login/vercel/callback-implicit/actions.ts @@ -0,0 +1,21 @@ +"use server"; + +import { cookies as getCookies } from "next/headers"; +import { validateImplicitAuthorization } from "../issuer"; + +export async function validateImplicitAuthorizationAction( + oidcParams: Record +) { + const { id_token, state } = oidcParams; + + const cookies = await getCookies(); + const expectedState = cookies.get("vercel-oidc-state")?.value || undefined; + + const claims = await validateImplicitAuthorization({ + id_token, + state, + expectedState, + }); + + return { claims }; +} diff --git a/app/login/vercel/callback-implicit/page.tsx b/app/login/vercel/callback-implicit/page.tsx new file mode 100644 index 0000000..279b430 --- /dev/null +++ b/app/login/vercel/callback-implicit/page.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { useEffect, useState, useTransition } from "react"; +import { validateImplicitAuthorizationAction } from "./actions"; +import { set } from "lodash"; + +export default function ImplicitOidcCallbackPage() { + const [oidcParams, setOidcParams] = useState>({}); + + useEffect(() => { + const hash = window.location.hash; + if (hash) { + const params = new URLSearchParams(hash.replace("#", "?")); + setOidcParams(Object.fromEntries(params)); + } + }, []); + + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(null); + const [claims, setClaims] = useState | null>(null); + + const handleValidate = () => { + setError(null); + startTransition(async () => { + try { + const response = await validateImplicitAuthorizationAction(oidcParams); + setClaims(response.claims); + } catch { + setError("Failed to validate OIDC authorization"); + } + }); + }; + + return ( +
+
+

+ Implicit OIDC Callback +

+
{JSON.stringify(oidcParams, null, 2)}
+
+ +
+
+ {isPending ? ( +

Validating...

+ ) : claims ? ( +
{JSON.stringify(claims, null, 2)}
+ ) : ( +
+ )} +
+ {error ?
{error}
: null} +
+
+ ); +} diff --git a/app/login/vercel/issuer.ts b/app/login/vercel/issuer.ts index c561953..124821a 100644 --- a/app/login/vercel/issuer.ts +++ b/app/login/vercel/issuer.ts @@ -5,6 +5,7 @@ import { Configuration, discovery, IDToken, + implicitAuthentication, randomState, } from "openid-client"; import { createRemoteJWKSet, jwtVerify } from "jose"; @@ -138,3 +139,21 @@ export async function getTokens( return { id_token, claims }; } + +export async function validateImplicitAuthorization({ + id_token, + state, + expectedState, +}: { + id_token: string; + state: string; + expectedState: string | undefined; +}): Promise { + const claims = await validateIdToken(id_token); + + if (state !== expectedState) { + throw new Error("State mismatch during implicit authorization validation."); + } + + return claims; +} diff --git a/app/login/vercel/prompt/page.tsx b/app/login/vercel/prompt/page.tsx index d8fa959..0a96555 100644 --- a/app/login/vercel/prompt/page.tsx +++ b/app/login/vercel/prompt/page.tsx @@ -1,4 +1,4 @@ -import { startAuthorization } from "../actions"; +import { startAuthorization, startImplicitAuthorization } from "../actions"; export default async function LoginVercelPage() { return ( @@ -15,6 +15,13 @@ export default async function LoginVercelPage() { > Login with explicit flow +