Skip to content

Commit

Permalink
feat: add state to logout url (#1646)
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity authored Oct 10, 2024
1 parent 6adc16d commit aa69da4
Show file tree
Hide file tree
Showing 33 changed files with 146 additions and 123 deletions.
6 changes: 3 additions & 3 deletions packages/ui/docs-bundle/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { extractBuildId, extractNextDataPathname } from "@/server/extractNextDataPathname";
import { getPageRoute, getPageRouteMatch, getPageRoutePath } from "@/server/pageRoutes";
import { rewritePosthog } from "@/server/rewritePosthog";
import { getXFernHostEdge } from "@/server/xfernhost/edge";
import { getDocsDomainEdge } from "@/server/xfernhost/edge";
import { withDefaultProtocol } from "@fern-api/ui-core-utils";
import type { FernUser } from "@fern-ui/fern-docs-auth";
import { getAuthEdgeConfig } from "@fern-ui/fern-docs-edge-config";
Expand All @@ -16,7 +16,7 @@ const API_FERN_DOCS_PATTERN = /^(?!\/api\/fern-docs\/).*(\/api\/fern-docs\/)/;
const CHANGELOG_PATTERN = /\.(rss|atom)$/;

export const middleware: NextMiddleware = async (request) => {
const xFernHost = getXFernHostEdge(request);
const xFernHost = getDocsDomainEdge(request);
const nextUrl = request.nextUrl.clone();
const headers = new Headers(request.headers);

Expand Down Expand Up @@ -138,7 +138,7 @@ export const middleware: NextMiddleware = async (request) => {
}

/**
* Rewrite all other requests to /static/[host]/[[...slug]] or /dynamic/[host]/[[...slug]]
* Rewrite all other requests to /static/[domain]/[[...slug]] or /dynamic/[domain]/[[...slug]]
*/

nextUrl.pathname = getPageRoute(!isDynamic, xFernHost, pathname);
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/docs-bundle/src/pages/_error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Error, { ErrorProps } from "next/error";
import { ReactElement } from "react";

export function parseResolvedUrl(resolvedUrl: string): string {
// if resolvedUrl is `/static/[host]/[...slug]` or `/dynamic/[host]/[..slug]` then return '/[...slug]`
// if resolvedUrl is `/static/[domain]/[...slug]` or `/dynamic/[domain]/[..slug]` then return '/[...slug]`
const match = resolvedUrl.match(/\/(static|dynamic)\/[^/]+(.*)/);
return match?.[2] ?? resolvedUrl;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { getXFernHostNode } from "@/server/xfernhost/node";
import { getDocsDomainNode } from "@/server/xfernhost/node";
import * as ApiDefinition from "@fern-api/fdr-sdk/api-definition";
import { getFeatureFlags } from "@fern-ui/fern-docs-edge-config";
import { ApiDefinitionLoader } from "@fern-ui/fern-docs-server";
import { getMdxBundler } from "@fern-ui/ui/bundlers";
import { NextApiHandler, NextApiResponse } from "next";

const resolveApiHandler: NextApiHandler = async (req, res: NextApiResponse<ApiDefinition.ApiDefinition>) => {
const xFernHost = getXFernHostNode(req);
const xFernHost = getDocsDomainNode(req);
const { api, endpoint } = req.query;
if (req.method !== "GET" || typeof api !== "string" || typeof endpoint !== "string") {
res.status(400).end();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { getXFernHostNode } from "@/server/xfernhost/node";
import { getDocsDomainNode } from "@/server/xfernhost/node";
import * as ApiDefinition from "@fern-api/fdr-sdk/api-definition";
import { getFeatureFlags } from "@fern-ui/fern-docs-edge-config";
import { ApiDefinitionLoader } from "@fern-ui/fern-docs-server";
import { getMdxBundler } from "@fern-ui/ui/bundlers";
import { NextApiHandler, NextApiResponse } from "next";

const resolveApiHandler: NextApiHandler = async (req, res: NextApiResponse<ApiDefinition.ApiDefinition>) => {
const xFernHost = getXFernHostNode(req);
const xFernHost = getDocsDomainNode(req);
const { api, webhook } = req.query;
if (req.method !== "GET" || typeof api !== "string" || typeof webhook !== "string") {
res.status(400).end();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { getXFernHostNode } from "@/server/xfernhost/node";
import { getDocsDomainNode } from "@/server/xfernhost/node";
import * as ApiDefinition from "@fern-api/fdr-sdk/api-definition";
import { getFeatureFlags } from "@fern-ui/fern-docs-edge-config";
import { ApiDefinitionLoader } from "@fern-ui/fern-docs-server";
import { getMdxBundler } from "@fern-ui/ui/bundlers";
import { NextApiHandler, NextApiResponse } from "next";

const resolveApiHandler: NextApiHandler = async (req, res: NextApiResponse<ApiDefinition.ApiDefinition>) => {
const xFernHost = getXFernHostNode(req);
const xFernHost = getDocsDomainNode(req);
const { api, websocket } = req.query;
if (req.method !== "GET" || typeof api !== "string" || typeof websocket !== "string") {
res.status(400).end();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { OAuth2Client } from "@/server/auth/OAuth2Client";
import { getAPIKeyInjectionConfig } from "@/server/auth/getApiKeyInjectionConfig";
import { withSecureCookie } from "@/server/auth/withSecure";
import { getXFernHostEdge } from "@/server/xfernhost/edge";
import { getDocsDomainEdge } from "@/server/xfernhost/edge";
import { APIKeyInjectionConfig, OryAccessTokenSchema } from "@fern-ui/fern-docs-auth";
import { getAuthEdgeConfig } from "@fern-ui/fern-docs-edge-config";
import { COOKIE_FERN_TOKEN } from "@fern-ui/fern-docs-utils";
Expand All @@ -12,7 +12,7 @@ import type { OauthScope } from "webflow-api/api/types/OAuthScope";
export const runtime = "edge";

export default async function handler(req: NextRequest): Promise<NextResponse<APIKeyInjectionConfig>> {
const domain = getXFernHostEdge(req);
const domain = getDocsDomainEdge(req);
const edgeConfig = await getAuthEdgeConfig(domain);

// assume that if the edge config is set for webflow, api key injection is always enabled
Expand Down
16 changes: 5 additions & 11 deletions packages/ui/docs-bundle/src/pages/api/fern-docs/auth/callback.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { signFernJWT } from "@/server/auth/FernJWT";
import { withSecureCookie } from "@/server/auth/withSecure";
import { redirectWithLoginError } from "@/server/redirectWithLoginError";
import { safeUrl } from "@/server/safeUrl";
import { getWorkOS, getWorkOSClientId } from "@/server/workos";
import { getXFernHostEdge, getXFernHostHeaderFallbackOrigin } from "@/server/xfernhost/edge";
import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge";
import { withDefaultProtocol } from "@fern-api/ui-core-utils";
import { FernUser } from "@fern-ui/fern-docs-auth";
import { getAuthEdgeConfig } from "@fern-ui/fern-docs-edge-config";
Expand All @@ -11,25 +12,18 @@ import { NextRequest, NextResponse } from "next/server";

export const runtime = "edge";

function redirectWithLoginError(location: string, errorMessage: string): NextResponse {
const url = new URL(location);
url.searchParams.set("loginError", errorMessage);
// TODO: validate allowlist of domains to prevent open redirects
return NextResponse.redirect(url.toString());
}

export default async function GET(req: NextRequest): Promise<NextResponse> {
if (req.method !== "GET") {
return new NextResponse(null, { status: 405 });
}
const domain = getXFernHostEdge(req);
const domain = getDocsDomainEdge(req);

// The authorization code returned by AuthKit
const code = req.nextUrl.searchParams.get("code");
const state = req.nextUrl.searchParams.get("state");
const error = req.nextUrl.searchParams.get("error");
const error_description = req.nextUrl.searchParams.get("error_description");
const redirectLocation = safeUrl(state) ?? withDefaultProtocol(getXFernHostHeaderFallbackOrigin(req));
const redirectLocation = safeUrl(state) ?? safeUrl(withDefaultProtocol(getHostEdge(req)));

if (error != null) {
return redirectWithLoginError(redirectLocation, error_description ?? error);
Expand Down Expand Up @@ -76,7 +70,7 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
const token = await signFernJWT(fernUser, user);

// TODO: validate allowlist of domains to prevent open redirects
const res = NextResponse.redirect(redirectLocation);
const res = redirectLocation ? NextResponse.redirect(redirectLocation) : NextResponse.next();
res.cookies.set(COOKIE_FERN_TOKEN, token, withSecureCookie());
return res;
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
import { verifyFernJWTConfig } from "@/server/auth/FernJWT";
import { withSecureCookie } from "@/server/auth/withSecure";
import { redirectWithLoginError } from "@/server/redirectWithLoginError";
import { safeUrl } from "@/server/safeUrl";
import { getXFernHostEdge, getXFernHostHeaderFallbackOrigin } from "@/server/xfernhost/edge";
import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge";
import { withDefaultProtocol } from "@fern-api/ui-core-utils";
import { getAuthEdgeConfig } from "@fern-ui/fern-docs-edge-config";
import { COOKIE_FERN_TOKEN } from "@fern-ui/fern-docs-utils";
import { NextRequest, NextResponse } from "next/server";

export const runtime = "edge";

function redirectWithLoginError(location: string, errorMessage: string): NextResponse {
const url = new URL(location);
url.searchParams.set("loginError", errorMessage);
// TODO: validate allowlist of domains to prevent open redirects
return NextResponse.redirect(url.toString());
}

export default async function handler(req: NextRequest): Promise<NextResponse> {
if (req.method !== "GET") {
return new NextResponse(null, { status: 405 });
}

const domain = getXFernHostEdge(req);
const domain = getDocsDomainEdge(req);
const edgeConfig = await getAuthEdgeConfig(domain);

// since we expect the callback to be redirected to, the token will be in the query params
const token = req.nextUrl.searchParams.get(COOKIE_FERN_TOKEN);
const state = req.nextUrl.searchParams.get("state");
const redirectLocation = safeUrl(state) ?? withDefaultProtocol(getXFernHostHeaderFallbackOrigin(req));
const redirectLocation = safeUrl(state) ?? safeUrl(withDefaultProtocol(getHostEdge(req)));

if (edgeConfig?.type !== "basic_token_verification" || token == null) {
// eslint-disable-next-line no-console
Expand All @@ -39,7 +33,7 @@ export default async function handler(req: NextRequest): Promise<NextResponse> {
await verifyFernJWTConfig(token, edgeConfig);

// TODO: validate allowlist of domains to prevent open redirects
const res = NextResponse.redirect(redirectLocation);
const res = redirectLocation ? NextResponse.redirect(redirectLocation) : NextResponse.next();
res.cookies.set(COOKIE_FERN_TOKEN, token, withSecureCookie());
return res;
} catch (e) {
Expand Down
18 changes: 11 additions & 7 deletions packages/ui/docs-bundle/src/pages/api/fern-docs/auth/logout.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { safeUrl } from "@/server/safeUrl";
import { getXFernHostEdge, getXFernHostHeaderFallbackOrigin } from "@/server/xfernhost/edge";
import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge";
import { withDefaultProtocol } from "@fern-api/ui-core-utils";
import { getAuthEdgeConfig } from "@fern-ui/fern-docs-edge-config";
import { COOKIE_ACCESS_TOKEN, COOKIE_FERN_TOKEN, COOKIE_REFRESH_TOKEN } from "@fern-ui/fern-docs-utils";
Expand All @@ -8,17 +8,21 @@ import { NextRequest, NextResponse } from "next/server";
export const runtime = "edge";

export default async function GET(req: NextRequest): Promise<NextResponse> {
const domain = getXFernHostEdge(req);
const domain = getDocsDomainEdge(req);

const authConfig = await getAuthEdgeConfig(domain);
const logoutUrl = authConfig?.type === "basic_token_verification" ? authConfig.logout : undefined;
const logoutUrl = authConfig?.type === "basic_token_verification" ? safeUrl(authConfig.logout) : undefined;

const state = req.nextUrl.searchParams.get("state");
const state = safeUrl(req.nextUrl.searchParams.get("state"));

const redirectLocation =
safeUrl(logoutUrl) ?? safeUrl(state) ?? withDefaultProtocol(getXFernHostHeaderFallbackOrigin(req));
// if logout url is provided, append the state to it before redirecting
if (logoutUrl != null && state != null) {
logoutUrl?.searchParams.set("state", state.toString());
}

const res = NextResponse.redirect(redirectLocation);
const redirectLocation = logoutUrl ?? state ?? safeUrl(withDefaultProtocol(getHostEdge(req)));

const res = redirectLocation ? NextResponse.redirect(redirectLocation) : NextResponse.next();
res.cookies.delete(COOKIE_FERN_TOKEN);
res.cookies.delete(COOKIE_ACCESS_TOKEN);
res.cookies.delete(COOKIE_REFRESH_TOKEN);
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/docs-bundle/src/pages/api/fern-docs/changelog.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DocsLoader } from "@/server/DocsLoader";
import { getXFernHostNode } from "@/server/xfernhost/node";
import { getDocsDomainNode } from "@/server/xfernhost/node";
import type { DocsV1Read } from "@fern-api/fdr-sdk/client/types";
import * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { NodeCollector } from "@fern-api/fdr-sdk/navigation";
Expand All @@ -24,7 +24,7 @@ export default async function responseApiHandler(req: NextApiRequest, res: NextA
return res.status(400).end();
}

const xFernHost = getXFernHostNode(req);
const xFernHost = getDocsDomainNode(req);

const fernToken = req.cookies[COOKIE_FERN_TOKEN];
const loader = DocsLoader.for(xFernHost, fernToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { getXFernHostEdge } from "@/server/xfernhost/edge";
import { getDocsDomainEdge } from "@/server/xfernhost/edge";
import { getFeatureFlags } from "@fern-ui/fern-docs-edge-config";
import { FeatureFlags } from "@fern-ui/fern-docs-utils";
import { NextRequest, NextResponse } from "next/server";

export const runtime = "edge";

export default async function handler(req: NextRequest): Promise<NextResponse<FeatureFlags>> {
const domain = getXFernHostEdge(req);
const domain = getDocsDomainEdge(req);
return NextResponse.json(await getFeatureFlags(domain));
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { verifyFernJWTConfig } from "@/server/auth/FernJWT";
import { getXFernHostEdge } from "@/server/xfernhost/edge";
import { getDocsDomainEdge } from "@/server/xfernhost/edge";
import { LaunchDarklyEdgeConfig, getAuthEdgeConfig, getLaunchDarklySettings } from "@fern-ui/fern-docs-edge-config";
import { COOKIE_EMAIL, COOKIE_FERN_TOKEN } from "@fern-ui/fern-docs-utils";
import { randomUUID } from "crypto";
Expand All @@ -24,7 +24,7 @@ interface LaunchDarklyInfo {
}

export default async function handler(req: NextRequest): Promise<NextResponse<LaunchDarklyInfo | undefined>> {
const domain = getXFernHostEdge(req);
const domain = getDocsDomainEdge(req);

const config = await safeGetLaunchDarklySettings(domain);
const clientSideId = config?.["client-side-id"];
Expand Down Expand Up @@ -68,7 +68,7 @@ async function getUserContext(req: NextRequest): Promise<LaunchDarklyInfo["user"

if (fernToken) {
try {
const user = await verifyFernJWTConfig(fernToken, await getAuthEdgeConfig(getXFernHostEdge(req)));
const user = await verifyFernJWTConfig(fernToken, await getAuthEdgeConfig(getDocsDomainEdge(req)));
const key = (await hashString(user.email)) ?? randomUUID();
return { key: `fern-docs-user-${key}`, email: user.email, name: user.name };
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getXFernHostHeaderFallbackOrigin } from "@/server/xfernhost/edge";
import { getHostEdge } from "@/server/xfernhost/edge";
import { withDefaultProtocol } from "@fern-api/ui-core-utils";
import { COOKIE_EMAIL } from "@fern-ui/fern-docs-utils";
import { NextRequest, NextResponse } from "next/server";
Expand All @@ -9,7 +9,7 @@ export default async function handler(req: NextRequest): Promise<NextResponse> {
const email = req.nextUrl.searchParams.get(COOKIE_EMAIL);

// TODO: validate allowlist of domains to prevent open redirects
const res = NextResponse.redirect(withDefaultProtocol(getXFernHostHeaderFallbackOrigin(req)));
const res = NextResponse.redirect(withDefaultProtocol(getHostEdge(req)));

if (email) {
res.cookies.set({ name: COOKIE_EMAIL, value: email });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { signFernJWT } from "@/server/auth/FernJWT";
import { OAuth2Client } from "@/server/auth/OAuth2Client";
import { withSecureCookie } from "@/server/auth/withSecure";
import { redirectWithLoginError } from "@/server/redirectWithLoginError";
import { safeUrl } from "@/server/safeUrl";
import { getXFernHostEdge, getXFernHostHeaderFallbackOrigin } from "@/server/xfernhost/edge";
import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge";
import { withDefaultProtocol } from "@fern-api/ui-core-utils";
import { FernUser, OryAccessTokenSchema } from "@fern-ui/fern-docs-auth";
import { getAuthEdgeConfig } from "@fern-ui/fern-docs-edge-config";
Expand All @@ -11,25 +12,18 @@ import { NextRequest, NextResponse } from "next/server";

export const runtime = "edge";

function redirectWithLoginError(location: string, errorMessage: string): NextResponse {
const url = new URL(location);
url.searchParams.set("loginError", errorMessage);
// TODO: validate allowlist of domains to prevent open redirects
return NextResponse.redirect(url.toString());
}

export default async function GET(req: NextRequest): Promise<NextResponse> {
if (req.method !== "GET") {
return new NextResponse(null, { status: 405 });
}

const domain = getXFernHostEdge(req);
const domain = getDocsDomainEdge(req);

const code = req.nextUrl.searchParams.get("code");
const state = req.nextUrl.searchParams.get("state");
const error = req.nextUrl.searchParams.get("error");
const error_description = req.nextUrl.searchParams.get("error_description");
const redirectLocation = safeUrl(state) ?? withDefaultProtocol(getXFernHostHeaderFallbackOrigin(req));
const redirectLocation = safeUrl(state) ?? safeUrl(withDefaultProtocol(getHostEdge(req)));

if (error != null) {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -61,7 +55,7 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
};
const expires = token.exp == null ? undefined : new Date(token.exp * 1000);
// TODO: validate allowlist of domains to prevent open redirects
const res = NextResponse.redirect(redirectLocation);
const res = redirectLocation ? NextResponse.redirect(redirectLocation) : NextResponse.next();
res.cookies.set(COOKIE_FERN_TOKEN, await signFernJWT(fernUser), withSecureCookie({ expires }));
res.cookies.set(COOKIE_ACCESS_TOKEN, access_token, withSecureCookie({ expires }));
if (refresh_token != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
import { withSecureCookie } from "@/server/auth/withSecure";
import { redirectWithLoginError } from "@/server/redirectWithLoginError";
import { safeUrl } from "@/server/safeUrl";
import { getXFernHostEdge, getXFernHostHeaderFallbackOrigin } from "@/server/xfernhost/edge";
import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge";
import { withDefaultProtocol } from "@fern-api/ui-core-utils";
import { getAuthEdgeConfig } from "@fern-ui/fern-docs-edge-config";
import { NextRequest, NextResponse } from "next/server";
import { WebflowClient } from "webflow-api";

export const runtime = "edge";

function redirectWithLoginError(location: string, errorMessage: string): NextResponse {
const url = new URL(location);
url.searchParams.set("loginError", errorMessage);
// TODO: validate allowlist of domains to prevent open redirects
return NextResponse.redirect(url.toString());
}

export default async function GET(req: NextRequest): Promise<NextResponse> {
if (req.method !== "GET") {
return new NextResponse(null, { status: 405 });
}

const domain = getXFernHostEdge(req);
const domain = getDocsDomainEdge(req);

const code = req.nextUrl.searchParams.get("code");
const state = req.nextUrl.searchParams.get("state");
const error = req.nextUrl.searchParams.get("error");
const error_description = req.nextUrl.searchParams.get("error_description");
const redirectLocation = safeUrl(state) ?? withDefaultProtocol(getXFernHostHeaderFallbackOrigin(req));
const redirectLocation = safeUrl(state) ?? safeUrl(withDefaultProtocol(getHostEdge(req)));

if (error != null) {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -57,7 +51,7 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
});

// TODO: validate allowlist of domains to prevent open redirects
const res = NextResponse.redirect(redirectLocation);
const res = redirectLocation ? NextResponse.redirect(redirectLocation) : NextResponse.next();
res.cookies.set("access_token", accessToken, withSecureCookie());
return res;
} catch (error) {
Expand Down
Loading

0 comments on commit aa69da4

Please sign in to comment.