Skip to content

Commit

Permalink
fix: redirect allowlist for authed previews (#1958)
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity authored Jan 7, 2025
1 parent afea2d0 commit ddea4ae
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 24 deletions.
20 changes: 11 additions & 9 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
{
"search.exclude": {
".vscode": true,
"pnpm-lock.yaml": true,
"**/.next": true,
"**/.pnp.*": true,
"**/.turbo": true,
"**/.yarn": true,
"**/node_modules": true,
"**/dist": true,
"**/__snapshots__/**": true,
"**/__test__/**/fixtures": true,
"**/__test__/**/output": true,
"**/build": true,
"**/out": true,
"**/dist": true,
"**/generated": true,
"**/lib": true,
"**/.next": true,
"**/node_modules": true,
"**/out": true,
"**/storybook-static": true,
"**/generated": true,
"**/__test__/**/fixtures": true,
"**/__test__/**/output": true,
"**/__snapshots__/**": true,
"**/*.tsbuildinfo": true,
"pnpm-lock.yaml": true,
"tests": true
},
"typescript.enablePromptUseWorkspaceTsdk": true,
Expand Down
6 changes: 4 additions & 2 deletions packages/fern-docs/bundle/src/server/FernNextResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ export class FernNextResponse {
!allowedDomains.includes(redirectLocation.host) &&
!isBuildWithFern(redirectLocation.host)
) {
// open redirect to unknown host detected:
return new NextResponse(null, { status: 410 });
console.error(
`Redirect to ${redirectLocation.host} is not allowed. Allowed domains: ${allowedDomains.join(", ")}`
);
return new NextResponse(null, { status: 403 });
}

return NextResponse.redirect(redirectLocation, init);
Expand Down
68 changes: 60 additions & 8 deletions packages/fern-docs/bundle/src/server/auth/allowed-redirects.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,75 @@
import { AuthEdgeConfig } from "@fern-docs/auth";
/**
* In order to prevent open-redirection, we need to curate a list of allowed domains where the server can redirect to.
*/

import { AuthEdgeConfig, OAuth2, SSOWorkOS } from "@fern-docs/auth";
import { PreviewUrlAuth } from "@fern-docs/edge-config";
import { compact } from "es-toolkit/array";
import { UnreachableCaseError } from "ts-essentials";

const WORKOS_API_URL = "https://api.workos.com";
const WEBFLOW_API_URL = "https://webflow.com";

export function getAllowedRedirectUrls(
authConfig?: AuthEdgeConfig | undefined
authConfig?: AuthEdgeConfig | undefined,
previewAuthConfig?: PreviewUrlAuth | undefined
): string[] {
return [
...getAllowedRedirectUrlsForAuthConfig(authConfig),
...getAllowedRedirectUrlsForPreviewAuthConfig(previewAuthConfig),
];
}

function getAllowedRedirectUrlsForAuthConfig(authConfig?: AuthEdgeConfig) {
if (authConfig == null) {
return [];
}

if (authConfig.type === "basic_token_verification") {
return compact([authConfig.redirect, authConfig.logout]);
switch (authConfig.type) {
case "basic_token_verification":
// since the `redirect` and `logout` are configured in the edge config, we can trust them
return compact([authConfig.redirect, authConfig.logout]);
case "sso":
return getAllowedRedirectUrlsForSSO(authConfig);
case "oauth2":
return getAllowedRedirectUrlsForOAuth2(authConfig);
default:
console.error(new UnreachableCaseError(authConfig));
}

return [];
}

function getAllowedRedirectUrlsForPreviewAuthConfig(
previewAuthConfig?: PreviewUrlAuth
) {
if (previewAuthConfig == null) {
return [];
}

switch (previewAuthConfig.type) {
case "workos":
return [WORKOS_API_URL];
default:
console.error(new UnreachableCaseError(previewAuthConfig.type));
}

if (authConfig.type === "sso") {
if (authConfig.partner === "workos") {
return compact([WORKOS_API_URL]);
}
return [];
}

function getAllowedRedirectUrlsForSSO(_authConfig: SSOWorkOS) {
return [WORKOS_API_URL];
}

function getAllowedRedirectUrlsForOAuth2(authConfig: OAuth2) {
switch (authConfig.partner) {
case "ory":
// since the environment is configured in the edge config, we can trust it
return [authConfig.environment];
case "webflow":
return [WEBFLOW_API_URL];
default:
console.error(new UnreachableCaseError(authConfig));
}

return [];
Expand Down
16 changes: 11 additions & 5 deletions packages/fern-docs/bundle/src/server/auth/getAuthState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,24 +175,30 @@ export async function getAuthState(
): Promise<AuthState & DomainAndHost> {
authConfig ??= await getAuthEdgeConfig(domain);
const orgMetadata = await getOrgMetadataForDomain(withoutStaging(domain));
const previewAuthConfig =
orgMetadata != null
? await getPreviewUrlAuthConfig(orgMetadata)
: undefined;

const authState = await getAuthStateInternal({
host,
fernToken,
pathname,
authConfig,
setFernToken,
previewAuthConfig:
orgMetadata != null
? await getPreviewUrlAuthConfig(orgMetadata)
: undefined,
previewAuthConfig,
});

const allowedDestinations = getAllowedRedirectUrls(
authConfig,
previewAuthConfig
);

return {
...authState,
domain,
host,
allowedDestinations: getAllowedRedirectUrls(authConfig),
allowedDestinations,
};
}

Expand Down

0 comments on commit ddea4ae

Please sign in to comment.