feat: v2 wildcard redirectURIs (#18165)

* feat: isOriginAllowed function

* feat: use isOriginAllowed in v2

* refactor: shorten isOriginAllowed
This commit is contained in:
Lauris Skraucis
2024-12-17 11:21:59 +00:00
committed by GitHub
parent c7dc3ecc49
commit b35dc43011
4 changed files with 87 additions and 2 deletions
@@ -0,0 +1,65 @@
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
describe("isOriginAllowed", () => {
describe("is allowed", () => {
it("should return true for exact match without wildcard", () => {
const allowedOrigins = ["https://app.cal.com/callback"];
const origin = "https://app.cal.com/callback";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
it("should return true for wildcard domain match", () => {
const allowedOrigins = ["*.multiscreen.d1test.biz"];
const origin = "https://sub.multiscreen.d1test.biz";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
it("should return true for wildcard pattern matching any domain", () => {
const allowedOrigins = ["*/callback"];
const origin = "https://another.com/callback";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
it("should handle a wildcard only pattern", () => {
const allowedOrigins = ["*"];
const origin = "https://any.domain.com/anypath";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
it("should handle multiple allowed URIs where only one matches", () => {
const allowedOrigins = [
"https://app.cal.com/other",
"*.notthisone.com",
"https://app.cal.com/callback",
];
const origin = "https://app.cal.com/callback";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
it("should handle patterns with multiple wildcards correctly", () => {
const allowedOrigins = ["https://*.mydomain.com/*"];
const origin = "https://sub.mydomain.com/path/to/resource";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
});
describe("is not allowed", () => {
it("should return false if no allowed patterns match", () => {
const allowedOrigins = ["https://app.cal.com/callback", "*.multiscreen.d1test.biz", "*/callback"];
const origin = "https://unknown.com";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(false);
});
it("should return false if wildcard pattern doesn't match the given origin", () => {
const allowedOrigins = ["*.example.com"];
const origin = "https://notexample.org";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(false);
});
it("should handle empty allowedUris array correctly", () => {
const allowedOrigins: string[] = [];
const origin = "https://app.cal.com/callback";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(false);
});
});
});
@@ -0,0 +1,18 @@
export function isOriginAllowed(origin: string, allowedOrigins: string[]): boolean {
return allowedOrigins.some((allowedOrigin) => {
if (allowedOrigin.includes("*")) {
return wildcardToRegex(allowedOrigin).test(origin);
}
return origin === allowedOrigin;
});
}
function wildcardToRegex(pattern: string): RegExp {
const escaped = escapeRegex(pattern);
const regexPattern = "^" + escaped.replace(/\\\*/g, ".*") + "$";
return new RegExp(regexPattern);
}
function escapeRegex(str: string): string {
return str.replace(/[.+*?^${}()|[\]\\]/g, "\\$&");
}
@@ -1,5 +1,6 @@
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
import { AuthMethods } from "@/lib/enums/auth-methods";
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
import { BaseStrategy } from "@/lib/passport/strategies/types";
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
import { DeploymentsService } from "@/modules/deployments/deployments.service";
@@ -167,7 +168,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
throw new UnauthorizedException("OAuth client not found given the access token");
}
if (origin && !client.redirectUris.some((uri) => uri.startsWith(origin))) {
if (origin && !isOriginAllowed(origin, client.redirectUris)) {
throw new UnauthorizedException(
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${origin}' to the 'Redirect uris' of your OAuth client.`
);
@@ -1,5 +1,6 @@
import { getEnv } from "@/env";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
@@ -62,7 +63,7 @@ export class OAuthFlowController {
throw new BadRequestException(`OAuth client with ID '${clientId}' not found`);
}
if (!oauthClient?.redirectUris.includes(body.redirectUri)) {
if (!isOriginAllowed(body.redirectUri, oauthClient.redirectUris)) {
throw new BadRequestException("Invalid 'redirect_uri' value.");
}