From 045f603849bf0d31af4d3da60222f43682609359 Mon Sep 17 00:00:00 2001 From: spastorelli Date: Thu, 16 Apr 2026 22:40:56 +0200 Subject: [PATCH] Add ~gitbook/auth/logout endpoint in GBO (#4195) --- .../[siteData]/~gitbook/auth/logout/route.ts | 36 +++++++++++++++++++ packages/gitbook/src/app/utils.ts | 2 +- packages/gitbook/src/lib/data/visitor.test.ts | 18 ---------- packages/gitbook/src/lib/data/visitor.ts | 2 +- packages/gitbook/src/middleware.ts | 18 ++++++---- 5 files changed, 50 insertions(+), 26 deletions(-) create mode 100644 packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/auth/logout/route.ts diff --git a/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/auth/logout/route.ts b/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/auth/logout/route.ts new file mode 100644 index 000000000..e3714719d --- /dev/null +++ b/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/auth/logout/route.ts @@ -0,0 +1,36 @@ +import { + type RouteLayoutParams, + getDynamicSiteContext, + getSiteURLDataFromParams, +} from '@/app/utils'; +import { getVisitorAuthBasePath } from '@/lib/data'; +import { getVisitorAuthCookieName } from '@/lib/visitors'; +import { cookies } from 'next/headers'; +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; + +/** + * Clear the site-scoped auth session cookies and redirect to the site root. + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise } +) { + const resolvedParams = await params; + const [{ context }, siteURLData, cookieStore] = await Promise.all([ + getDynamicSiteContext(resolvedParams), + getSiteURLDataFromParams(resolvedParams), + cookies(), + ]); + + cookieStore.delete( + getVisitorAuthCookieName( + getVisitorAuthBasePath(new URL(request.nextUrl.toString()), siteURLData) + ) + ); + + // TODO: Redirect to the site root for now. Once the API supports it, + // optionally redirect to a logoutURL (e.g when needing to logout from upstream auth too) + // when defined in visitor auth settings. + return NextResponse.redirect(context.linker.toAbsoluteURL(context.linker.toPathInSite(''))); +} diff --git a/packages/gitbook/src/app/utils.ts b/packages/gitbook/src/app/utils.ts index ff6fd6ffc..ca0c944fc 100644 --- a/packages/gitbook/src/app/utils.ts +++ b/packages/gitbook/src/app/utils.ts @@ -119,7 +119,7 @@ function getModeFromParams(mode: string): RouteParamMode { /** * Get the decoded site data from the params. */ -function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData { +export function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData { try { const decoded = decodeURIComponent(params.siteData); return rison.decode(decoded); diff --git a/packages/gitbook/src/lib/data/visitor.test.ts b/packages/gitbook/src/lib/data/visitor.test.ts index 373dcbc47..880fa7f13 100644 --- a/packages/gitbook/src/lib/data/visitor.test.ts +++ b/packages/gitbook/src/lib/data/visitor.test.ts @@ -7,16 +7,7 @@ describe('getVisitorAuthBasePath', () => { getVisitorAuthBasePath( new URL('https://proxy.gitbook.site/sites/site_foo/hello/world'), { - site: 'site_foo', - siteSpace: 'sitesp_foo', - basePath: '/foo', siteBasePath: '/foo', - organization: 'org_foo', - space: 'space_foo', - pathname: '/hello/world', - complete: false, - apiToken: 'api_token_foo', - canonicalUrl: 'https://example.com/docs/foo/hello/world', } ) ).toBe('/sites/site_foo/'); @@ -25,16 +16,7 @@ describe('getVisitorAuthBasePath', () => { it('should return the correct base path for non-proxy requests', () => { expect( getVisitorAuthBasePath(new URL('https://example.com/docs/foo/hello/world'), { - site: 'site_foo', - siteSpace: 'sitesp_foo', - basePath: '/foo/', siteBasePath: '/foo/', - organization: 'org_foo', - space: 'space_foo', - pathname: '/hello/world', - complete: false, - apiToken: 'api_token_foo', - canonicalUrl: 'https://example.com/docs/foo/hello/world', }) ).toBe('/foo/'); }); diff --git a/packages/gitbook/src/lib/data/visitor.ts b/packages/gitbook/src/lib/data/visitor.ts index c49c4fd9b..eb8705bf9 100644 --- a/packages/gitbook/src/lib/data/visitor.ts +++ b/packages/gitbook/src/lib/data/visitor.ts @@ -7,7 +7,7 @@ import type { PublishedSiteContent } from '@gitbook/api'; */ export function getVisitorAuthBasePath( siteRequestURL: URL, - siteURLData: PublishedSiteContent + siteURLData: Pick ): string { // The siteRequestURL for proxy requests is of the form `https://proxy.gitbook.com/site/siteId/...` // In such cases, we should not use the resolved siteBasePath for the cookie because for subsequent requests diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index 745763f42..a09a999dd 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -4,6 +4,7 @@ import { SiteInsightsLLMSVariant, } from '@gitbook/api'; import { shouldServeMarkdown } from '@vercel/agent-readability'; +import { cookies } from 'next/headers'; import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import rison from 'rison'; @@ -43,7 +44,6 @@ import { } from '@/lib/visitors'; import { waitUntil } from '@/lib/waitUntil'; import { serveResizedImage } from '@/routes/image'; -import { cookies } from 'next/headers'; import { type ServerInsightsEventInput, serveProxyAnalyticsEvent, @@ -305,12 +305,17 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { }); } - cookies.push( - ...getResponseCookiesForVisitorAuth( - getVisitorAuthBasePath(siteRequestURL, siteURLData), - visitorToken - ) + const normalizedSitePathname = removeLeadingSlash( + removeTrailingSlash(siteURLData.pathname) ); + if (normalizedSitePathname !== '~gitbook/auth/logout') { + cookies.push( + ...getResponseCookiesForVisitorAuth( + getVisitorAuthBasePath(siteRequestURL, siteURLData), + visitorToken + ) + ); + } // We use the host/origin from the canonical URL to ensure the links are // correctly generated when the site is proxied. e.g. https://proxy.gitbook.com/site/siteId/... @@ -755,6 +760,7 @@ function encodePathInSiteContent( case '~gitbook/pdf': case '~gitbook/search': case '~gitbook/auth/login': + case '~gitbook/auth/logout': case '~scalar/proxy': // PDF, search and auth routes are always dynamic as they depend on the request. return { pathname, routeType: 'dynamic' };