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 (