Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions app/login/vercel/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
21 changes: 21 additions & 0 deletions app/login/vercel/callback-implicit/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use server";

import { cookies as getCookies } from "next/headers";
import { validateImplicitAuthorization } from "../issuer";

export async function validateImplicitAuthorizationAction(
oidcParams: Record<string, string>
) {
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 };
}
63 changes: 63 additions & 0 deletions app/login/vercel/callback-implicit/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Record<string, string>>({});

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<string | null>(null);
const [claims, setClaims] = useState<Record<string, any> | 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 (
<div className="bg-gray-100 h-screen flex items-center justify-center">
<div className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 w-full max-w-sm flex flex-col gap-4">
<h1 className="block text-gray-700 text-xl font-bold mb-6 text-center">
Implicit OIDC Callback
</h1>
<pre>{JSON.stringify(oidcParams, null, 2)}</pre>
<form method="POST" action={handleValidate}>
<button
type="submit"
disabled={isPending || !oidcParams.id_token}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
>
Validate response
</button>
</form>
<div>
{isPending ? (
<p>Validating...</p>
) : claims ? (
<pre>{JSON.stringify(claims, null, 2)}</pre>
) : (
<div />
)}
</div>
{error ? <div className="bg-red-600">{error}</div> : null}
</div>
</div>
);
}
19 changes: 19 additions & 0 deletions app/login/vercel/issuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Configuration,
discovery,
IDToken,
implicitAuthentication,
randomState,
} from "openid-client";
import { createRemoteJWKSet, jwtVerify } from "jose";
Expand Down Expand Up @@ -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<IDToken> {
const claims = await validateIdToken(id_token);

if (state !== expectedState) {
throw new Error("State mismatch during implicit authorization validation.");
}

return claims;
}
9 changes: 8 additions & 1 deletion app/login/vercel/prompt/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { startAuthorization } from "../actions";
import { startAuthorization, startImplicitAuthorization } from "../actions";

export default async function LoginVercelPage() {
return (
Expand All @@ -15,6 +15,13 @@ export default async function LoginVercelPage() {
>
Login with explicit flow
</button>
<button
type="submit"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
formAction={startImplicitAuthorization}
>
Login with implicit flow
</button>
</form>
</div>
</div>
Expand Down