Skip to content

Commit

Permalink
updating open redirects
Browse files Browse the repository at this point in the history
  • Loading branch information
fern authored and fern committed Dec 9, 2024
1 parent c5a40ad commit 58dea91
Show file tree
Hide file tree
Showing 10 changed files with 37 additions and 27 deletions.
10 changes: 6 additions & 4 deletions packages/ui/docs-bundle/src/pages/api/fern-docs/auth/callback.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getReturnToQueryParam } from "@/server/auth/return-to";
import { FernNextResponse } from "@/server/FernNextResponse";
import { redirectWithLoginError } from "@/server/redirectWithLoginError";
import { safeUrl } from "@/server/safeUrl";
import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge";
Expand Down Expand Up @@ -26,11 +27,12 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
const redirectLocation = safeUrl(return_to) ?? safeUrl(withDefaultProtocol(host));

if (error != null) {
return redirectWithLoginError(redirectLocation, error, error_description);
return redirectWithLoginError(req, redirectLocation, error, error_description);
}

if (typeof code !== "string") {
return redirectWithLoginError(
req,
redirectLocation,
"missing_authorization_code",
"Couldn't login, please try again",
Expand All @@ -51,11 +53,11 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
"/api/fern-docs/auth/callback",
"/api/fern-docs/oauth/ory/callback",
);
return NextResponse.redirect(nextUrl);
return FernNextResponse.redirect(req, nextUrl.toString());
} else if (config?.type === "sso") {
nextUrl.pathname = nextUrl.pathname.replace("/api/fern-docs/auth/callback", "/api/fern-docs/auth/sso/callback");
return NextResponse.redirect(nextUrl);
return FernNextResponse.redirect(req, nextUrl.toString());
}

return redirectWithLoginError(redirectLocation, "unknown_error", "Couldn't login, please try again");
return redirectWithLoginError(req, redirectLocation, "unknown_error", "Couldn't login, please try again");
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { safeVerifyFernJWTConfig } from "@/server/auth/FernJWT";
import { getReturnToQueryParam } from "@/server/auth/return-to";
import { withSecureCookie } from "@/server/auth/with-secure-cookie";
import { FernNextResponse } from "@/server/FernNextResponse";
import { redirectWithLoginError } from "@/server/redirectWithLoginError";
import { safeUrl } from "@/server/safeUrl";
import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge";
Expand Down Expand Up @@ -28,17 +29,16 @@ export default async function handler(req: NextRequest): Promise<NextResponse> {
if (edgeConfig?.type !== "basic_token_verification" || token == null) {
// eslint-disable-next-line no-console
console.error(`Invalid config for domain ${domain}`);
return redirectWithLoginError(redirectLocation, "unknown_error", "Couldn't login, please try again");
return redirectWithLoginError(req, redirectLocation, "unknown_error", "Couldn't login, please try again");
}

const fernUser = await safeVerifyFernJWTConfig(token, edgeConfig);

if (fernUser == null) {
return redirectWithLoginError(redirectLocation, "unknown_error", "Couldn't login, please try again");
return redirectWithLoginError(req, redirectLocation, "unknown_error", "Couldn't login, please try again");
}

// TODO: validate allowlist of domains to prevent open redirects
const res = redirectLocation ? NextResponse.redirect(redirectLocation) : NextResponse.next();
const res = redirectLocation ? FernNextResponse.redirect(req, redirectLocation.toString()) : NextResponse.next();
res.cookies.set(COOKIE_FERN_TOKEN, token, withSecureCookie(withDefaultProtocol(host)));
return res;
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
let redirectLocation =
logoutUrl ??
safeUrl(req.nextUrl.searchParams.get(return_to_param)) ??
safeUrl(withDefaultProtocol(getHostEdge(req)));
safeUrl(withDefaultProtocol(getHostEdge(req))) ??
new URL(domain);

const res = FernNextResponse.redirect(req, redirectLocation);
const res = FernNextResponse.redirect(req, redirectLocation.toString());
res.cookies.delete(withDeleteCookie(COOKIE_FERN_TOKEN, withDefaultProtocol(getHostEdge(req))));
res.cookies.delete(withDeleteCookie(COOKIE_ACCESS_TOKEN, withDefaultProtocol(getHostEdge(req))));
res.cookies.delete(withDeleteCookie(COOKIE_REFRESH_TOKEN, withDefaultProtocol(getHostEdge(req))));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getReturnToQueryParam } from "@/server/auth/return-to";
import { withSecureCookie } from "@/server/auth/with-secure-cookie";
import { getWorkOSClientId, workos } from "@/server/auth/workos";
import { encryptSession } from "@/server/auth/workos-session";
import { FernNextResponse } from "@/server/FernNextResponse";
import { safeUrl } from "@/server/safeUrl";
import { getDocsDomainEdge } from "@/server/xfernhost/edge";
import { COOKIE_FERN_TOKEN } from "@fern-ui/fern-docs-utils";
Expand Down Expand Up @@ -58,7 +59,7 @@ export default async function handler(req: NextRequest): Promise<NextResponse> {
// TODO: need to support docs instances with subpaths (forward-proxied from the origin).
const destination = new URL(`${req.nextUrl.pathname}${req.nextUrl.search}`, url.origin);
destination.searchParams.set(FORWARDED_HOST_QUERY, req.nextUrl.host);
return NextResponse.redirect(destination);
return FernNextResponse.redirect(req, destination.toString());
}

const code = req.nextUrl.searchParams.get(CODE_QUERY);
Expand Down Expand Up @@ -86,7 +87,7 @@ export default async function handler(req: NextRequest): Promise<NextResponse> {
impersonator,
});

const res = NextResponse.redirect(url);
const res = FernNextResponse.redirect(req, url.toString());
res.cookies.set(COOKIE_FERN_TOKEN, session, withSecureCookie(url.origin));

return res;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FernNextResponse } from "@/server/FernNextResponse";
import { getHostEdge } from "@/server/xfernhost/edge";
import { withDefaultProtocol } from "@fern-api/ui-core-utils";
import { COOKIE_EMAIL } from "@fern-ui/fern-docs-utils";
Expand All @@ -8,8 +9,7 @@ export const runtime = "edge";
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(getHostEdge(req)));
const res = FernNextResponse.redirect(req, 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
Expand Up @@ -2,6 +2,7 @@ import { signFernJWT } from "@/server/auth/FernJWT";
import { OryOAuth2Client } from "@/server/auth/ory";
import { getReturnToQueryParam } from "@/server/auth/return-to";
import { withSecureCookie } from "@/server/auth/with-secure-cookie";
import { FernNextResponse } from "@/server/FernNextResponse";
import { redirectWithLoginError } from "@/server/redirectWithLoginError";
import { safeUrl } from "@/server/safeUrl";
import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge";
Expand Down Expand Up @@ -31,13 +32,14 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
if (error != null) {
// eslint-disable-next-line no-console
console.error(`OAuth2 error: ${error} - ${error_description}`);
return redirectWithLoginError(redirectLocation, error, error_description);
return redirectWithLoginError(req, redirectLocation, error, error_description);
}

if (typeof code !== "string") {
// eslint-disable-next-line no-console
console.error("Missing code in query params");
return redirectWithLoginError(
req,
redirectLocation,
"missing_authorization_code",
"Couldn't login, please try again",
Expand All @@ -47,7 +49,7 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
if (config == null || config.type !== "oauth2" || config.partner !== "ory") {
// eslint-disable-next-line no-console
console.log(`Invalid config for domain ${domain}`);
return redirectWithLoginError(redirectLocation, "unknown_error", "Couldn't login, please try again");
return redirectWithLoginError(req, redirectLocation, "unknown_error", "Couldn't login, please try again");
}

const oauthClient = new OryOAuth2Client(config);
Expand All @@ -60,7 +62,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 = redirectLocation ? NextResponse.redirect(redirectLocation) : NextResponse.next();
const res = redirectLocation ? FernNextResponse.redirect(req, redirectLocation.toString()) : NextResponse.next();
res.cookies.set(
COOKIE_FERN_TOKEN,
await signFernJWT(fernUser),
Expand All @@ -82,6 +84,6 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error getting access token", error);
return redirectWithLoginError(redirectLocation, "unknown_error", "Couldn't login, please try again");
return redirectWithLoginError(req, redirectLocation, "unknown_error", "Couldn't login, please try again");
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getReturnToQueryParam } from "@/server/auth/return-to";
import { withSecureCookie } from "@/server/auth/with-secure-cookie";
import { FernNextResponse } from "@/server/FernNextResponse";
import { redirectWithLoginError } from "@/server/redirectWithLoginError";
import { safeUrl } from "@/server/safeUrl";
import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge";
Expand Down Expand Up @@ -28,13 +29,14 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
if (error != null) {
// eslint-disable-next-line no-console
console.error(`OAuth2 error: ${error} - ${error_description}`);
return redirectWithLoginError(redirectLocation, error, error_description);
return redirectWithLoginError(req, redirectLocation, error, error_description);
}

if (typeof code !== "string") {
// eslint-disable-next-line no-console
console.error("Missing code in query params");
return redirectWithLoginError(
req,
redirectLocation,
"missing_authorization_code",
"Couldn't login, please try again",
Expand All @@ -44,7 +46,7 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
if (config == null || config.type !== "oauth2" || config.partner !== "webflow") {
// eslint-disable-next-line no-console
console.log(`Invalid config for domain ${domain}`);
return redirectWithLoginError(redirectLocation, "unknown_error", "Couldn't login, please try again");
return redirectWithLoginError(req, redirectLocation, "unknown_error", "Couldn't login, please try again");
}

try {
Expand All @@ -55,13 +57,12 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
code,
});

// TODO: validate allowlist of domains to prevent open redirects
const res = redirectLocation ? NextResponse.redirect(redirectLocation) : NextResponse.next();
const res = redirectLocation ? FernNextResponse.redirect(req, redirectLocation.toString()) : NextResponse.next();
res.cookies.set("access_token", accessToken, withSecureCookie(withDefaultProtocol(host)));
return res;
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error getting access token", error);
return redirectWithLoginError(redirectLocation, "unknown_error", "Couldn't login, please try again");
return redirectWithLoginError(req, redirectLocation, "unknown_error", "Couldn't login, please try again");
}
}
2 changes: 1 addition & 1 deletion packages/ui/docs-bundle/src/server/FernNextResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from "next/server";
import { getDocsDomainEdge } from "./xfernhost/edge";

export class FernNextResponse {
public static redirect(req: NextRequest, destination?: string): Promise<NextResponse> {
public static redirect(req: NextRequest, destination?: string): NextResponse {
if (typeof destination === "undefined") {
return NextResponse.next();
}
Expand Down
6 changes: 4 additions & 2 deletions packages/ui/docs-bundle/src/server/redirectWithLoginError.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import { FernNextResponse } from "./FernNextResponse";

export function redirectWithLoginError(
request: NextRequest,
location: URL | undefined,
error: string,
error_description: string | null | undefined,
Expand All @@ -15,5 +17,5 @@ export function redirectWithLoginError(
url.searchParams.set("error_description", error_description);
}
// TODO: validate allowlist of domains to prevent open redirects
return NextResponse.redirect(url.toString());
return FernNextResponse.redirect(request, url.toString());
}
3 changes: 2 additions & 1 deletion packages/ui/docs-bundle/src/server/withMiddlewareAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { COOKIE_FERN_TOKEN } from "@fern-ui/fern-docs-utils";
import { NextRequest, NextResponse } from "next/server";
import { getAuthStateEdge } from "./auth/getAuthStateEdge";
import { withSecureCookie } from "./auth/with-secure-cookie";
import { FernNextResponse } from "./FernNextResponse";
import { getHostEdge } from "./xfernhost/edge";

/**
Expand Down Expand Up @@ -41,7 +42,7 @@ export async function withMiddlewareAuth(
}

if (res.authorizationUrl) {
return NextResponse.redirect(res.authorizationUrl);
return FernNextResponse.redirect(request, res.authorizationUrl);
}

return NextResponse.next({ status: 401 });
Expand Down

0 comments on commit 58dea91

Please sign in to comment.