feat: v2 wildcard redirectURIs (#18165)
* feat: isOriginAllowed function * feat: use isOriginAllowed in v2 * refactor: shorten isOriginAllowed
This commit is contained in:
@@ -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.`
|
||||
);
|
||||
|
||||
+2
-1
@@ -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.");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user