Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: redirect_uri should be set in edge config #1591

Merged
merged 2 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions packages/ui/app/src/auth/OAuth2Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,15 @@ export class OAuth2Client {
private readonly environment: string;
private readonly scope: string | undefined;
private readonly jwks: string | undefined;
private readonly redirectUri?: string;

constructor(
config: AuthEdgeConfigOAuth2Ory,
private readonly redirect_uri?: string,
) {
constructor(config: AuthEdgeConfigOAuth2Ory) {
this.clientId = config.clientId;
this.clientSecret = config.clientSecret;
this.environment = config.environment;
this.scope = config.scope;
this.jwks = config.jwks;
this.redirectUri = config.redirectUri;
}

public async getToken(code: string): Promise<OAuthTokenResponse> {
Expand All @@ -34,8 +33,8 @@ export class OAuth2Client {
form.append("client_secret", this.clientSecret);
form.append("grant_type", "authorization_code");
form.append("client_id", this.clientId);
if (this.redirect_uri != null) {
form.append("redirect_uri", this.redirect_uri);
if (this.redirectUri != null) {
form.append("redirect_uri", this.redirectUri);
}

const response = await fetch(urlJoin(this.environment, "/token"), {
Expand All @@ -55,8 +54,8 @@ export class OAuth2Client {
form.append("client_secret", this.clientSecret);
form.append("grant_type", "refresh_token");
form.append("client_id", this.clientId);
if (this.redirect_uri != null) {
form.append("redirect_uri", this.redirect_uri);
if (this.redirectUri != null) {
form.append("redirect_uri", this.redirectUri);
}

const response = await fetch(urlJoin(this.environment, "/token"), {
Expand All @@ -74,8 +73,8 @@ export class OAuth2Client {
const url = new URL(urlJoin(this.environment, "/auth"));
url.searchParams.set("response_type", "code");
url.searchParams.set("client_id", this.clientId);
if (this.redirect_uri != null) {
url.searchParams.set("redirect_uri", this.redirect_uri);
if (this.redirectUri != null) {
url.searchParams.set("redirect_uri", this.redirectUri);
}
if (state != null) {
url.searchParams.set("state", state);
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/app/src/auth/getApiKeyInjectionConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function getAPIKeyInjectionConfig(
): Promise<APIKeyInjectionConfig> {
const config = await getAuthEdgeConfig(domain);
if (config?.type === "oauth2" && config.partner === "ory" && config["api-key-injection-enabled"]) {
const client = new OAuth2Client(config, `https://${domain}/api/auth/callback`);
const client = new OAuth2Client(config);
const tokens = cookies != null ? await client.getOrRefreshAccessTokenEdge(cookies) : undefined;

if (tokens != null) {
Expand Down Expand Up @@ -71,7 +71,7 @@ export async function getAPIKeyInjectionConfigNode(
): Promise<APIKeyInjectionConfig> {
const config = await getAuthEdgeConfig(domain);
if (config?.type === "oauth2" && config.partner === "ory" && config["api-key-injection-enabled"]) {
const client = new OAuth2Client(config, `https://${domain}/api/auth/callback`);
const client = new OAuth2Client(config);
const tokens = cookies != null ? await client.getOrRefreshAccessTokenNode(cookies) : undefined;

if (tokens != null) {
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/app/src/auth/getAuthEdgeConfig.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { captureMessage } from "@sentry/nextjs";
import { get } from "@vercel/edge-config";
import { AuthEdgeConfig, AuthEdgeConfigSchema } from "./types";

Expand All @@ -8,8 +9,17 @@ export async function getAuthEdgeConfig(currentDomain: string): Promise<AuthEdge
const toRet = domainToTokenConfigMap?.[currentDomain];

if (toRet != null) {
// if the config is present, it should be valid
return AuthEdgeConfigSchema.parse(toRet);
const config = AuthEdgeConfigSchema.safeParse(toRet);

// if the config is present, it should be valid.
// if it's malformed, custom auth for this domain will not work and may leak docs to the public.
if (!config.success) {
// eslint-disable-next-line no-console
console.error(config.error);
captureMessage(`Could not parse AuthEdgeConfigSchema for ${currentDomain}`, "fatal");
}

return config.data;
}

return;
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/app/src/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const AuthEdgeConfigOAuth2OrySchema = z.object({
scope: z.optional(z.string()),
clientId: z.string(),
clientSecret: z.string(),
redirectUri: z.string().optional(),
"api-key-injection-enabled": z.optional(z.boolean()),
});
export const AuthEdgeConfigOAuth2WebflowSchema = z.object({
Expand All @@ -25,6 +26,7 @@ export const AuthEdgeConfigOAuth2WebflowSchema = z.object({
scope: z.optional(z.union([z.string(), z.array(z.string())])),
clientId: z.string(),
clientSecret: z.string(),
redirectUri: z.string().optional(),
});

export const AuthEdgeConfigBasicTokenVerificationSchema = z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ export function PlaygroundAuthorizationFormCard({
}
url.searchParams.set("state", state.toString());

if (apiKeyInjection.partner === "ory") {
if (apiKeyInjection.partner === "ory" || apiKeyInjection.partner === "webflow") {
const redirect_uri = urlJoin(window.location.origin, callbackApiRoute);
url.searchParams.set("redirect_uri", redirect_uri);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
withSecureCookie,
} from "@fern-ui/ui/auth";
import { NextRequest, NextResponse } from "next/server";
import urlJoin from "url-join";
import { WebflowClient } from "webflow-api";
import type { OauthScope } from "webflow-api/api/types/OAuthScope";

Expand All @@ -27,12 +26,10 @@ export default async function handler(req: NextRequest): Promise<NextResponse<AP
authenticated: false,
url: WebflowClient.authorizeURL({
clientId: edgeConfig.clientId,

// TODO: subpaths will not work
// redirectUri: `https://${domain}/api/fern-docs/oauth/webflow/callback`,
redirectUri: edgeConfig.redirectUri,

// note: this is not validated
scope: (edgeConfig.scope as OauthScope | OauthScope[]) ?? "authorized_user:read",
scope: (edgeConfig.scope as OauthScope | OauthScope[]) ?? [],
}),
});
}
Expand All @@ -59,7 +56,7 @@ export default async function handler(req: NextRequest): Promise<NextResponse<AP
let exp = expires;

if (edgeConfig != null && edgeConfig.type === "oauth2" && edgeConfig.partner === "ory") {
const oauthClient = new OAuth2Client(edgeConfig, urlJoin(`https://${domain}/api/auth/callback`));
const oauthClient = new OAuth2Client(edgeConfig);

const token = OryAccessTokenSchema.parse(await oauthClient.decode(access_token));
exp = token.exp == null ? undefined : new Date(token.exp * 1000);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
if (req.method !== "GET") {
return new NextResponse(null, { status: 405 });
}
const domain = getXFernHostEdge(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 = state ?? req.nextUrl.origin;
const redirectLocation = state ?? `https://${domain}/`;

if (error != null) {
return redirectWithLoginError(redirectLocation, error_description ?? error);
Expand All @@ -31,7 +32,6 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
return redirectWithLoginError(redirectLocation, "Couldn't login, please try again");
}

const domain = getXFernHostEdge(req);
const config = await getAuthEdgeConfig(domain);

if (config != null && config.type === "oauth2" && config.partner === "ory") {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { getXFernHostEdge } from "@/server/xfernhost/edge";
import { NextRequest, NextResponse } from "next/server";

export const runtime = "edge";

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

const state = req.nextUrl.searchParams.get("state");
const redirectLocation = state ?? req.nextUrl.origin;
const redirectLocation = state ?? `https://${domain}/`;

const res = NextResponse.redirect(redirectLocation);
res.cookies.delete("fern_token");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
withSecureCookie,
} from "@fern-ui/ui/auth";
import { NextRequest, NextResponse } from "next/server";
import urlJoin from "url-join";

export const runtime = "edge";

Expand All @@ -23,11 +22,13 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
return new NextResponse(null, { status: 405 });
}

const domain = getXFernHostEdge(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 = state ?? req.nextUrl.origin;
const redirectLocation = state ?? `https://${domain}/`;

if (error != null) {
return redirectWithLoginError(redirectLocation, error_description ?? error);
Expand All @@ -37,14 +38,13 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
return redirectWithLoginError(redirectLocation, "Couldn't login, please try again");
}

const domain = getXFernHostEdge(req);
const config = await getAuthEdgeConfig(domain);

if (config == null || config.type !== "oauth2" || config.partner !== "ory") {
return redirectWithLoginError(redirectLocation, "Couldn't login, please try again");
}

const oauthClient = new OAuth2Client(config, urlJoin(`https://${domain}`, req.nextUrl.pathname));
const oauthClient = new OAuth2Client(config);
try {
const { access_token, refresh_token } = await oauthClient.getToken(code);
const token = OryAccessTokenSchema.parse(await oauthClient.decode(access_token));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
return new NextResponse(null, { status: 405 });
}

const domain = getXFernHostEdge(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 = state ?? req.nextUrl.origin;
const redirectLocation = state ?? `https://${domain}/`;

if (error != null) {
return redirectWithLoginError(redirectLocation, error_description ?? error);
Expand All @@ -30,7 +32,6 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
return redirectWithLoginError(redirectLocation, "Couldn't login, please try again");
}

const domain = getXFernHostEdge(req);
const config = await getAuthEdgeConfig(domain);

if (config == null || config.type !== "oauth2" || config.partner === "ory") {
Expand All @@ -41,6 +42,7 @@ export default async function GET(req: NextRequest): Promise<NextResponse> {
const accessToken = await WebflowClient.getAccessToken({
clientId: config.clientId,
clientSecret: config.clientSecret,
redirectUri: config.redirectUri,
code,
});

Expand Down
Loading