Permission check for the protected handler (#161)
* Permission check for the protected handler * Add tests
This commit is contained in:
parent
6ffaba2dab
commit
bb3396dde6
6 changed files with 121 additions and 3 deletions
|
@ -2,6 +2,7 @@ import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
|
|||
|
||||
import { APL } from "../../APL";
|
||||
import { createDebug } from "../../debug";
|
||||
import { AppPermission } from "../../types";
|
||||
import { ProtectedHandlerContext } from "./process-async-saleor-webhook";
|
||||
import {
|
||||
processSaleorProtectedHandler,
|
||||
|
@ -33,12 +34,17 @@ export type NextProtectedApiHandler<TResp = unknown> = (
|
|||
* Also provides additional `context` object containing request properties.
|
||||
*/
|
||||
export const createProtectedHandler =
|
||||
(handlerFn: NextProtectedApiHandler, apl: APL): NextApiHandler =>
|
||||
(
|
||||
handlerFn: NextProtectedApiHandler,
|
||||
apl: APL,
|
||||
requiredPermissions?: AppPermission[]
|
||||
): NextApiHandler =>
|
||||
(req, res) => {
|
||||
debug("Protected handler called");
|
||||
processSaleorProtectedHandler({
|
||||
req,
|
||||
apl,
|
||||
requiredPermissions,
|
||||
})
|
||||
.then(async (context) => {
|
||||
debug("Incoming request validated. Call handlerFn");
|
||||
|
|
|
@ -4,6 +4,7 @@ import { APL } from "../../APL";
|
|||
import { AuthData } from "../../APL/apl";
|
||||
import { createDebug } from "../../debug";
|
||||
import { getBaseUrl, getSaleorHeaders } from "../../headers";
|
||||
import { AppPermission } from "../../types";
|
||||
import { verifyJWT } from "../../verify-jwt";
|
||||
|
||||
const debug = createDebug("processProtectedHandler");
|
||||
|
@ -38,6 +39,7 @@ export type ProtectedHandlerContext = {
|
|||
interface ProcessSaleorProtectedHandlerArgs {
|
||||
req: NextApiRequest;
|
||||
apl: APL;
|
||||
requiredPermissions?: AppPermission[];
|
||||
}
|
||||
|
||||
type ProcessAsyncSaleorProtectedHandler = (
|
||||
|
@ -51,8 +53,10 @@ type ProcessAsyncSaleorProtectedHandler = (
|
|||
export const processSaleorProtectedHandler: ProcessAsyncSaleorProtectedHandler = async ({
|
||||
req,
|
||||
apl,
|
||||
requiredPermissions,
|
||||
}: ProcessSaleorProtectedHandlerArgs): Promise<ProtectedHandlerContext> => {
|
||||
debug("Request processing started");
|
||||
|
||||
const { saleorApiUrl, authorizationBearer: token } = getSaleorHeaders(req.headers);
|
||||
|
||||
const baseUrl = getBaseUrl(req.headers);
|
||||
|
@ -85,7 +89,7 @@ export const processSaleorProtectedHandler: ProcessAsyncSaleorProtectedHandler =
|
|||
}
|
||||
|
||||
try {
|
||||
await verifyJWT({ appId: authData.appId, token, saleorApiUrl });
|
||||
await verifyJWT({ appId: authData.appId, token, saleorApiUrl, requiredPermissions });
|
||||
} catch (e) {
|
||||
throw new ProtectedHandlerError("JWT verification failed: ", "JWT_VERIFICATION_FAILED");
|
||||
}
|
||||
|
|
36
src/has-permissions-in-jwt-token.ts
Normal file
36
src/has-permissions-in-jwt-token.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { createDebug } from "./debug";
|
||||
import { AppPermission } from "./types";
|
||||
import { DashboardTokenPayload } from "./verify-jwt";
|
||||
|
||||
const debug = createDebug("checkJwtPermissions");
|
||||
|
||||
export const hasPermissionsInJwtToken = (
|
||||
tokenData?: Pick<DashboardTokenPayload, "user_permissions">,
|
||||
permissionsToCheckAgainst?: AppPermission[]
|
||||
) => {
|
||||
debug(`Permissions required ${permissionsToCheckAgainst}`);
|
||||
|
||||
if (!permissionsToCheckAgainst?.length) {
|
||||
debug("No permissions specified, check passed");
|
||||
return true;
|
||||
}
|
||||
|
||||
const userPermissions = tokenData?.user_permissions || undefined;
|
||||
|
||||
if (!userPermissions?.length) {
|
||||
debug("User has no permissions assigned. Rejected");
|
||||
return false;
|
||||
}
|
||||
|
||||
const arePermissionsSatisfied = permissionsToCheckAgainst.every((permission) =>
|
||||
userPermissions.includes(permission)
|
||||
);
|
||||
|
||||
if (!arePermissionsSatisfied) {
|
||||
debug("Permissions check not passed");
|
||||
return false;
|
||||
}
|
||||
|
||||
debug("Permissions check successful");
|
||||
return true;
|
||||
};
|
47
src/has-permissions.in-jwt-token.test.ts
Normal file
47
src/has-permissions.in-jwt-token.test.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { hasPermissionsInJwtToken } from "./has-permissions-in-jwt-token";
|
||||
import { DashboardTokenPayload } from "./verify-jwt";
|
||||
|
||||
describe("hasPermissionsInJwtToken", () => {
|
||||
it("Pass if no required permissions, and user has none", async () => {
|
||||
const tokenData: Pick<DashboardTokenPayload, "user_permissions"> = {
|
||||
user_permissions: [],
|
||||
};
|
||||
await expect(hasPermissionsInJwtToken(tokenData)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Pass if no required permissions", async () => {
|
||||
const tokenData: Pick<DashboardTokenPayload, "user_permissions"> = {
|
||||
user_permissions: ["MANAGE_ORDERS"],
|
||||
};
|
||||
await expect(hasPermissionsInJwtToken(tokenData)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Pass if user has assigned required permissions", async () => {
|
||||
const tokenData: Pick<DashboardTokenPayload, "user_permissions"> = {
|
||||
user_permissions: ["MANAGE_ORDERS", "MANAGE_CHECKOUTS", "HANDLE_TAXES"],
|
||||
};
|
||||
await expect(
|
||||
hasPermissionsInJwtToken(tokenData, ["MANAGE_ORDERS", "MANAGE_CHECKOUTS"])
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Reject if user is missing any of required permissions", async () => {
|
||||
const tokenData: Pick<DashboardTokenPayload, "user_permissions"> = {
|
||||
user_permissions: ["MANAGE_ORDERS", "HANDLE_TAXES"],
|
||||
};
|
||||
await expect(
|
||||
hasPermissionsInJwtToken(tokenData, ["MANAGE_ORDERS", "MANAGE_CHECKOUTS"])
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Reject if user is missing permission data", async () => {
|
||||
const tokenData: Pick<DashboardTokenPayload, "user_permissions"> = {
|
||||
user_permissions: [],
|
||||
};
|
||||
await expect(
|
||||
hasPermissionsInJwtToken(tokenData, ["MANAGE_ORDERS", "MANAGE_CHECKOUTS"])
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -41,4 +41,15 @@ describe("verifyJWT", () => {
|
|||
verifyJWT({ appId: "wrong_id", saleorApiUrl: validApiUrl, token: validToken })
|
||||
).rejects.toThrow("JWT verification failed: Token's app property is different than app ID.");
|
||||
});
|
||||
|
||||
it("Throw error on user missing the permissions", async () => {
|
||||
await expect(
|
||||
verifyJWT({
|
||||
appId: validAppId,
|
||||
saleorApiUrl: validApiUrl,
|
||||
token: validToken,
|
||||
requiredPermissions: ["HANDLE_TAXES"],
|
||||
})
|
||||
).rejects.toThrow("JWT verification failed: Token's permissions are not sufficient.");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,21 +1,30 @@
|
|||
import * as jose from "jose";
|
||||
|
||||
import { createDebug } from "./debug";
|
||||
import { hasPermissionsInJwtToken } from "./has-permissions-in-jwt-token";
|
||||
import { AppPermission } from "./types";
|
||||
import { getJwksUrlFromSaleorApiUrl } from "./urls";
|
||||
|
||||
const debug = createDebug("verify-jwt");
|
||||
|
||||
export interface DashboardTokenPayload extends jose.JWTPayload {
|
||||
app: string;
|
||||
user_permissions: AppPermission[];
|
||||
}
|
||||
|
||||
export interface verifyJWTArguments {
|
||||
appId: string;
|
||||
saleorApiUrl: string;
|
||||
token: string;
|
||||
requiredPermissions?: AppPermission[];
|
||||
}
|
||||
|
||||
export const verifyJWT = async ({ saleorApiUrl, token, appId }: verifyJWTArguments) => {
|
||||
export const verifyJWT = async ({
|
||||
saleorApiUrl,
|
||||
token,
|
||||
appId,
|
||||
requiredPermissions,
|
||||
}: verifyJWTArguments) => {
|
||||
let tokenClaims: DashboardTokenPayload;
|
||||
const ERROR_MESSAGE = "JWT verification failed:";
|
||||
|
||||
|
@ -35,6 +44,11 @@ export const verifyJWT = async ({ saleorApiUrl, token, appId }: verifyJWTArgumen
|
|||
throw new Error(`${ERROR_MESSAGE} Token's app property is different than app ID.`);
|
||||
}
|
||||
|
||||
if (!hasPermissionsInJwtToken(tokenClaims, requiredPermissions)) {
|
||||
debug("Token did not meet requirements for permissions: %s", requiredPermissions);
|
||||
throw new Error(`${ERROR_MESSAGE} Token's permissions are not sufficient.`);
|
||||
}
|
||||
|
||||
try {
|
||||
debug("Trying to create JWKS");
|
||||
|
||||
|
|
Loading…
Reference in a new issue