diff --git a/dev-auth/oidc-provider.mjs b/dev-auth/oidc-provider.mjs index d15deb4a..2e7702d1 100644 --- a/dev-auth/oidc-provider.mjs +++ b/dev-auth/oidc-provider.mjs @@ -43,6 +43,13 @@ const configuration = { "http://localhost:3003/api/auth/oauth2/callback/oidc", "http://localhost:3000/api/auth/oauth2/callback/okta", ], + // Post-logout redirect URIs for RP-Initiated Logout + post_logout_redirect_uris: [ + "http://localhost:3000/signin", + "http://localhost:3001/signin", + "http://localhost:3002/signin", + "http://localhost:3003/signin", + ], response_types: ["code"], grant_types: ["authorization_code", "refresh_token"], token_endpoint_auth_method: "client_secret_post", @@ -77,6 +84,19 @@ const configuration = { // Disable built-in dev interactions in favor of our custom auto-login // and auto-consent middleware used for local testing. devInteractions: { enabled: false }, + // Enable RP-Initiated Logout for sign-out flow + rpInitiatedLogout: { + enabled: true, + // Auto-confirm logout for dev (skip confirmation page) + logoutSource: async (ctx, form) => { + ctx.body = ` + Logging out... + + ${form} + + `; + }, + }, }, // Explicitly declare supported scopes, including offline_access for refresh tokens // Scopes supported by the dev provider (app requests offline_access in dev and prod) diff --git a/src/app/api/auth/refresh-token/route.ts b/src/app/api/auth/refresh-token/route.ts index bfe8dacc..588696f6 100644 --- a/src/app/api/auth/refresh-token/route.ts +++ b/src/app/api/auth/refresh-token/route.ts @@ -1,8 +1,11 @@ -import { cookies } from "next/headers"; +import { cookies, headers } from "next/headers"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; -import { refreshAccessToken } from "@/lib/auth/auth"; -import { BETTER_AUTH_SECRET, COOKIE_NAME } from "@/lib/auth/constants"; +import { auth, refreshAccessToken } from "@/lib/auth/auth"; +import { + BETTER_AUTH_SECRET, + OIDC_TOKEN_COOKIE_NAME, +} from "@/lib/auth/constants"; import type { OidcTokenData } from "@/lib/auth/types"; import { decrypt } from "@/lib/auth/utils"; @@ -13,6 +16,16 @@ import { decrypt } from "@/lib/auth/utils"; */ export async function POST(request: NextRequest) { try { + // Check if Better Auth session exists before attempting token refresh + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session?.user?.id) { + // No active session - skip token refresh (user is logged out) + return NextResponse.json({ error: "No active session" }, { status: 401 }); + } + const body = await request.json(); const { userId } = body; @@ -20,8 +33,13 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Missing userId" }, { status: 400 }); } + // Verify userId matches the session + if (userId !== session.user.id) { + return NextResponse.json({ error: "User ID mismatch" }, { status: 401 }); + } + const cookieStore = await cookies(); - const encryptedCookie = cookieStore.get(COOKIE_NAME); + const encryptedCookie = cookieStore.get(OIDC_TOKEN_COOKIE_NAME); if (!encryptedCookie?.value) { return NextResponse.json({ error: "No token found" }, { status: 401 }); @@ -32,32 +50,33 @@ export async function POST(request: NextRequest) { tokenData = await decrypt(encryptedCookie.value, BETTER_AUTH_SECRET); } catch (error) { console.error("[Refresh API] Token decryption failed:", error); - cookieStore.delete(COOKIE_NAME); + cookieStore.delete(OIDC_TOKEN_COOKIE_NAME); return NextResponse.json({ error: "Invalid token" }, { status: 401 }); } if (tokenData.userId !== userId) { console.error("[Refresh API] Token userId mismatch"); - cookieStore.delete(COOKIE_NAME); + cookieStore.delete(OIDC_TOKEN_COOKIE_NAME); return NextResponse.json({ error: "Invalid token" }, { status: 401 }); } if (!tokenData.refreshToken) { console.error("[Refresh API] No refresh token available"); - cookieStore.delete(COOKIE_NAME); + cookieStore.delete(OIDC_TOKEN_COOKIE_NAME); return NextResponse.json({ error: "No refresh token" }, { status: 401 }); } // Call refreshAccessToken which will save the new token in the cookie - const refreshedData = await refreshAccessToken( - tokenData.refreshToken, + const refreshedData = await refreshAccessToken({ + refreshToken: tokenData.refreshToken, + refreshTokenExpiresAt: tokenData.refreshTokenExpiresAt, userId, - tokenData.refreshTokenExpiresAt, - ); + idToken: tokenData.idToken, + }); if (!refreshedData) { console.error("[Refresh API] Token refresh failed"); - cookieStore.delete(COOKIE_NAME); + cookieStore.delete(OIDC_TOKEN_COOKIE_NAME); return NextResponse.json( { error: "[Refresh API] Refresh failed" }, { status: 401 }, diff --git a/src/app/catalog/page.tsx b/src/app/catalog/page.tsx index dd17ff61..e623747c 100644 --- a/src/app/catalog/page.tsx +++ b/src/app/catalog/page.tsx @@ -1,18 +1,7 @@ -import { headers } from "next/headers"; -import { redirect } from "next/navigation"; -import { auth } from "@/lib/auth/auth"; import { getServers } from "./actions"; import { ServersWrapper } from "./components/servers-wrapper"; export default async function CatalogPage() { - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session) { - redirect("/signin"); - } - const servers = await getServers(); return ; diff --git a/src/app/signin/signin-button.tsx b/src/app/signin/signin-button.tsx index 4f918000..3ce217ca 100644 --- a/src/app/signin/signin-button.tsx +++ b/src/app/signin/signin-button.tsx @@ -42,7 +42,7 @@ export function SignInButton({ providerId }: { providerId: string }) { return (