Skip to content

Commit 6a96190

Browse files
dcl10claude
andauthored
Feature/sign up page (#47)
* Implement sign-up via Keycloak registration page, remove /sign-up placeholder Adds signUpWithKeycloak server action which passes prompt=create to the Keycloak authorisation URL, directing users to Keycloak's hosted registration form. Sign up buttons in the TopBar and hero CTA are updated to use this action via native form submission, consistent with the sign-in pattern. The /sign-up placeholder page is removed. Closes #44 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add sign-out action and button to dashboard placeholder Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix sign-out not ending the Keycloak SSO session next-auth's signOut only cleared the local JWT cookie, leaving Keycloak's SSO session intact. Signing back in would silently re-authenticate the same user without prompting for credentials. Store the ID token in the JWT and session on initial sign-in, then use it as id_token_hint when redirecting to Keycloak's end-session endpoint so the Keycloak session is properly terminated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent bff3001 commit 6a96190

6 files changed

Lines changed: 66 additions & 50 deletions

File tree

frontend/app/_LandingCTA.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import Link from "next/link";
2-
3-
import { signInWithKeycloak } from "./actions";
1+
import { signInWithKeycloak, signUpWithKeycloak } from "./actions";
42

53
/**
64
* Hero call-to-action buttons for the landing page.
7-
* Sign in uses a server action so the OAuth redirect is issued as an HTTP
8-
* response, avoiding client-side router state corruption on back navigation.
5+
* Both actions use server actions so OAuth redirects are issued as HTTP
6+
* responses, avoiding client-side router state corruption on back navigation.
97
*/
108
export function LandingCTA() {
119
return (
@@ -18,12 +16,14 @@ export function LandingCTA() {
1816
Sign in
1917
</button>
2018
</form>
21-
<Link
22-
href="/sign-up"
23-
className="inline-flex items-center gap-2 font-sans font-medium tracking-[-0.005em] rounded-sm px-[16px] py-[9px] text-[14px] bg-nottingham-blue text-paper border border-nottingham-blue hover:bg-[var(--color-primary-deep)] transition-colors duration-[120ms]"
24-
>
25-
Sign up
26-
</Link>
19+
<form action={signUpWithKeycloak}>
20+
<button
21+
type="submit"
22+
className="inline-flex items-center gap-2 font-sans font-medium tracking-[-0.005em] rounded-sm px-[16px] py-[9px] text-[14px] bg-nottingham-blue text-paper border border-nottingham-blue hover:bg-[var(--color-primary-deep)] transition-colors duration-[120ms] cursor-pointer"
23+
>
24+
Sign up
25+
</button>
26+
</form>
2727
</div>
2828
);
2929
}

frontend/app/actions.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use server";
22

3-
import { signIn } from "@/auth";
3+
import { redirect } from "next/navigation";
4+
5+
import { auth, signIn, signOut } from "@/auth";
46

57
/**
68
* Initiates the Keycloak OAuth flow via a server action.
@@ -11,3 +13,35 @@ import { signIn } from "@/auth";
1113
export async function signInWithKeycloak() {
1214
await signIn("keycloak", { redirectTo: "/dashboard" });
1315
}
16+
17+
/**
18+
* Initiates Keycloak's registration flow via a server action.
19+
* The prompt=create authorisation parameter tells Keycloak to open its
20+
* registration page rather than the login page.
21+
*/
22+
export async function signUpWithKeycloak() {
23+
await signIn("keycloak", { redirectTo: "/dashboard" }, { prompt: "create" });
24+
}
25+
26+
/**
27+
* Signs the user out locally and ends the Keycloak SSO session.
28+
* next-auth's signOut only clears the local cookie; without also calling
29+
* Keycloak's end-session endpoint the SSO session stays alive and the user
30+
* is silently re-authenticated on the next sign-in attempt.
31+
*/
32+
export async function signOutFromKeycloak() {
33+
const session = await auth();
34+
const idToken = session?.idToken;
35+
36+
await signOut({ redirect: false });
37+
38+
const params = new URLSearchParams({
39+
post_logout_redirect_uri: `${process.env.AUTH_URL ?? "http://localhost:3000"}/`,
40+
client_id: process.env.AUTH_KEYCLOAK_ID!,
41+
...(idToken ? { id_token_hint: idToken } : {}),
42+
});
43+
44+
redirect(
45+
`${process.env.AUTH_KEYCLOAK_ISSUER}/protocol/openid-connect/logout?${params}`,
46+
);
47+
}

frontend/app/dashboard/page.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { redirect } from "next/navigation";
22

3+
import { signOutFromKeycloak } from "@/app/actions";
34
import { getSession } from "@/lib/auth";
45

56
export default async function DashboardPage() {
@@ -11,9 +12,17 @@ export default async function DashboardPage() {
1112
<h1 className="text-3xl font-bold tracking-[-0.025em] text-ink mb-3">
1213
Your project dashboard
1314
</h1>
14-
<p className="text-[15px] text-ink-soft">
15+
<p className="text-[15px] text-ink-soft mb-8">
1516
Dashboard functionality coming soon.
1617
</p>
18+
<form action={signOutFromKeycloak}>
19+
<button
20+
type="submit"
21+
className="inline-flex items-center gap-2 font-sans font-medium tracking-[-0.005em] rounded-sm px-[16px] py-[9px] text-[14px] bg-paper text-ink border border-[var(--border-strong)] hover:bg-portland-stone transition-colors duration-[120ms] cursor-pointer"
22+
>
23+
Sign out
24+
</button>
25+
</form>
1726
</main>
1827
);
1928
}

frontend/app/sign-up/page.tsx

Lines changed: 0 additions & 20 deletions
This file was deleted.

frontend/auth.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import Keycloak from "next-auth/providers/keycloak";
55
declare module "next-auth" {
66
interface Session {
77
accessToken?: string;
8+
idToken?: string;
89
error?: "RefreshAccessTokenError";
910
}
1011
}
1112

1213
declare module "@auth/core/jwt" {
1314
interface JWT {
1415
accessToken?: string;
16+
idToken?: string;
1517
refreshToken?: string;
1618
accessTokenExpires?: number;
1719
error?: "RefreshAccessTokenError";
@@ -57,6 +59,7 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
5759
return {
5860
...token,
5961
accessToken: account.access_token,
62+
idToken: account.id_token,
6063
refreshToken: account.refresh_token,
6164
accessTokenExpires: account.expires_at
6265
? account.expires_at * 1000
@@ -79,6 +82,7 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
7982

8083
async session({ session, token }) {
8184
session.accessToken = token.accessToken;
85+
session.idToken = token.idToken;
8286
session.error = token.error;
8387
return session;
8488
},

frontend/components/shared/LandingTopBar.tsx

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
"use client";
22

3-
import Link from "next/link";
4-
5-
import { signInWithKeycloak } from "@/app/actions";
3+
import { signInWithKeycloak, signUpWithKeycloak } from "@/app/actions";
64
import { Button } from "@/components/core";
75
import { useViewport } from "@/lib/hooks/useViewport";
8-
import { cn } from "@/lib/utils";
96

107
/**
118
* Sticky navigation bar for unauthenticated visitors.
@@ -67,19 +64,11 @@ export function LandingTopBar() {
6764
Sign in
6865
</Button>
6966
</form>
70-
<Link
71-
href="/sign-up"
72-
className={cn(
73-
"inline-flex items-center gap-2 font-sans font-medium tracking-[-0.005em] rounded-sm",
74-
"transition-[background-color,transform,border-color] duration-[120ms] ease-[cubic-bezier(0.2,0,0,1)]",
75-
"bg-nottingham-blue text-paper border border-nottingham-blue hover:bg-[var(--color-primary-deep)]",
76-
isMobile
77-
? "px-[10px] py-[5px] text-[12px]"
78-
: "px-[16px] py-[9px] text-[14px]",
79-
)}
80-
>
81-
Sign up
82-
</Link>
67+
<form action={signUpWithKeycloak}>
68+
<Button variant="primary" size={isMobile ? "sm" : "md"} type="submit">
69+
Sign up
70+
</Button>
71+
</form>
8372
</div>
8473
</header>
8574
);

0 commit comments

Comments
 (0)