diff --git a/tests/oauth-scopes.test.ts b/tests/oauth-scopes.test.ts new file mode 100644 index 00000000..0ccf2d12 --- /dev/null +++ b/tests/oauth-scopes.test.ts @@ -0,0 +1,95 @@ +import { checkIfOauthIsValid } from "@oauth"; +import { describe, expect, it } from "bun:test"; + +describe("checkIfOauthIsValid", () => { + it("should return true when routeScopes and application.scopes are empty", () => { + const application = { scopes: "" }; + const routeScopes: string[] = []; + const result = checkIfOauthIsValid(application as any, routeScopes); + expect(result).toBe(true); + }); + + it("should return true when routeScopes is empty and application.scopes contains write:* or write", () => { + const application = { scopes: "write:*" }; + const routeScopes: string[] = []; + const result = checkIfOauthIsValid(application as any, routeScopes); + expect(result).toBe(true); + }); + + it("should return true when routeScopes is empty and application.scopes contains read:* or read", () => { + const application = { scopes: "read:*" }; + const routeScopes: string[] = []; + const result = checkIfOauthIsValid(application as any, routeScopes); + expect(result).toBe(true); + }); + + it("should return true when routeScopes contains only write: permissions and application.scopes contains write:* or write", () => { + const application = { scopes: "write:*" }; + const routeScopes = ["write:users", "write:posts"]; + const result = checkIfOauthIsValid(application as any, routeScopes); + expect(result).toBe(true); + }); + + it("should return true when routeScopes contains only read: permissions and application.scopes contains read:* or read", () => { + const application = { scopes: "read:*" }; + const routeScopes = ["read:users", "read:posts"]; + const result = checkIfOauthIsValid(application as any, routeScopes); + expect(result).toBe(true); + }); + + it("should return true when routeScopes contains both write: and read: permissions and application.scopes contains write:* or write and read:* or read", () => { + const application = { scopes: "write:* read:*" }; + const routeScopes = ["write:users", "read:posts"]; + const result = checkIfOauthIsValid(application as any, routeScopes); + expect(result).toBe(true); + }); + + it("should return false when routeScopes contains write: permissions but application.scopes does not contain write:* or write", () => { + const application = { scopes: "read:*" }; + const routeScopes = ["write:users", "write:posts"]; + const result = checkIfOauthIsValid(application as any, routeScopes); + expect(result).toBe(false); + }); + + it("should return false when routeScopes contains read: permissions but application.scopes does not contain read:* or read", () => { + const application = { scopes: "write:*" }; + const routeScopes = ["read:users", "read:posts"]; + const result = checkIfOauthIsValid(application as any, routeScopes); + expect(result).toBe(false); + }); + + it("should return false when routeScopes contains both write: and read: permissions but application.scopes does not contain write:* or write and read:* or read", () => { + const application = { scopes: "" }; + const routeScopes = ["write:users", "read:posts"]; + const result = checkIfOauthIsValid(application as any, routeScopes); + expect(result).toBe(false); + }); + + it("should return true when routeScopes contains a mix of valid and invalid permissions and application.scopes contains all the required permissions", () => { + const application = { scopes: "write:* read:*" }; + const routeScopes = ["write:users", "invalid:permission", "read:posts"]; + const result = checkIfOauthIsValid(application as any, routeScopes); + expect(result).toBe(true); + }); + + it("should return false when routeScopes contains a mix of valid and invalid permissions but application.scopes does not contain all the required permissions", () => { + const application = { scopes: "write:*" }; + const routeScopes = ["write:users", "invalid:permission", "read:posts"]; + const result = checkIfOauthIsValid(application as any, routeScopes); + expect(result).toBe(false); + }); + + it("should return true when routeScopes contains a mix of valid write and read permissions and application.scopes contains all the required permissions", () => { + const application = { scopes: "write:* read:posts" }; + const routeScopes = ["write:users", "read:posts"]; + const result = checkIfOauthIsValid(application as any, routeScopes); + expect(result).toBe(true); + }); + + it("should return false when routeScopes contains a mix of valid write and read permissions but application.scopes does not contain all the required permissions", () => { + const application = { scopes: "write:*" }; + const routeScopes = ["write:users", "read:posts"]; + const result = checkIfOauthIsValid(application as any, routeScopes); + expect(result).toBe(false); + }); +}); diff --git a/utils/oauth.ts b/utils/oauth.ts new file mode 100644 index 00000000..9b971886 --- /dev/null +++ b/utils/oauth.ts @@ -0,0 +1,61 @@ +import { Application } from "@prisma/client"; + +/** + * Check if an OAuth application is valid for a route + * @param application The OAuth application + * @param routeScopes The scopes required for the route + * @returns Whether the OAuth application is valid for the route + */ +export const checkIfOauthIsValid = ( + application: Application, + routeScopes: string[] +) => { + if (routeScopes.length === 0) { + return true; + } + + const hasAllWriteScopes = + application.scopes.split(" ").includes("write:*") || + application.scopes.split(" ").includes("write"); + + const hasAllReadScopes = + application.scopes.split(" ").includes("read:*") || + application.scopes.split(" ").includes("read"); + + if (hasAllWriteScopes && hasAllReadScopes) { + return true; + } + + let nonMatchedScopes = routeScopes; + + if (hasAllWriteScopes) { + // Filter out all write scopes as valid + nonMatchedScopes = routeScopes.filter( + scope => !scope.startsWith("write:") + ); + } + + if (hasAllReadScopes) { + // Filter out all read scopes as valid + nonMatchedScopes = routeScopes.filter( + scope => !scope.startsWith("read:") + ); + } + + // If there are still scopes left, check if they match + // If there are no scopes left, return true + if (nonMatchedScopes.length === 0) { + return true; + } + + // If there are scopes left, check if they match + if ( + nonMatchedScopes.every(scope => + application.scopes.split(" ").includes(scope) + ) + ) { + return true; + } + + return false; +};