Permission check for the protected handler (#161)

* Permission check for the protected handler

* Add tests
This commit is contained in:
Krzysztof Wolski 2023-01-25 12:00:22 +01:00 committed by GitHub
parent 6ffaba2dab
commit bb3396dde6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 121 additions and 3 deletions

View file

@ -2,6 +2,7 @@ import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
import { APL } from "../../APL"; import { APL } from "../../APL";
import { createDebug } from "../../debug"; import { createDebug } from "../../debug";
import { AppPermission } from "../../types";
import { ProtectedHandlerContext } from "./process-async-saleor-webhook"; import { ProtectedHandlerContext } from "./process-async-saleor-webhook";
import { import {
processSaleorProtectedHandler, processSaleorProtectedHandler,
@ -33,12 +34,17 @@ export type NextProtectedApiHandler<TResp = unknown> = (
* Also provides additional `context` object containing request properties. * Also provides additional `context` object containing request properties.
*/ */
export const createProtectedHandler = export const createProtectedHandler =
(handlerFn: NextProtectedApiHandler, apl: APL): NextApiHandler => (
handlerFn: NextProtectedApiHandler,
apl: APL,
requiredPermissions?: AppPermission[]
): NextApiHandler =>
(req, res) => { (req, res) => {
debug("Protected handler called"); debug("Protected handler called");
processSaleorProtectedHandler({ processSaleorProtectedHandler({
req, req,
apl, apl,
requiredPermissions,
}) })
.then(async (context) => { .then(async (context) => {
debug("Incoming request validated. Call handlerFn"); debug("Incoming request validated. Call handlerFn");

View file

@ -4,6 +4,7 @@ import { APL } from "../../APL";
import { AuthData } from "../../APL/apl"; import { AuthData } from "../../APL/apl";
import { createDebug } from "../../debug"; import { createDebug } from "../../debug";
import { getBaseUrl, getSaleorHeaders } from "../../headers"; import { getBaseUrl, getSaleorHeaders } from "../../headers";
import { AppPermission } from "../../types";
import { verifyJWT } from "../../verify-jwt"; import { verifyJWT } from "../../verify-jwt";
const debug = createDebug("processProtectedHandler"); const debug = createDebug("processProtectedHandler");
@ -38,6 +39,7 @@ export type ProtectedHandlerContext = {
interface ProcessSaleorProtectedHandlerArgs { interface ProcessSaleorProtectedHandlerArgs {
req: NextApiRequest; req: NextApiRequest;
apl: APL; apl: APL;
requiredPermissions?: AppPermission[];
} }
type ProcessAsyncSaleorProtectedHandler = ( type ProcessAsyncSaleorProtectedHandler = (
@ -51,8 +53,10 @@ type ProcessAsyncSaleorProtectedHandler = (
export const processSaleorProtectedHandler: ProcessAsyncSaleorProtectedHandler = async ({ export const processSaleorProtectedHandler: ProcessAsyncSaleorProtectedHandler = async ({
req, req,
apl, apl,
requiredPermissions,
}: ProcessSaleorProtectedHandlerArgs): Promise<ProtectedHandlerContext> => { }: ProcessSaleorProtectedHandlerArgs): Promise<ProtectedHandlerContext> => {
debug("Request processing started"); debug("Request processing started");
const { saleorApiUrl, authorizationBearer: token } = getSaleorHeaders(req.headers); const { saleorApiUrl, authorizationBearer: token } = getSaleorHeaders(req.headers);
const baseUrl = getBaseUrl(req.headers); const baseUrl = getBaseUrl(req.headers);
@ -85,7 +89,7 @@ export const processSaleorProtectedHandler: ProcessAsyncSaleorProtectedHandler =
} }
try { try {
await verifyJWT({ appId: authData.appId, token, saleorApiUrl }); await verifyJWT({ appId: authData.appId, token, saleorApiUrl, requiredPermissions });
} catch (e) { } catch (e) {
throw new ProtectedHandlerError("JWT verification failed: ", "JWT_VERIFICATION_FAILED"); throw new ProtectedHandlerError("JWT verification failed: ", "JWT_VERIFICATION_FAILED");
} }

View 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;
};

View 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();
});
});

View file

@ -41,4 +41,15 @@ describe("verifyJWT", () => {
verifyJWT({ appId: "wrong_id", saleorApiUrl: validApiUrl, token: validToken }) verifyJWT({ appId: "wrong_id", saleorApiUrl: validApiUrl, token: validToken })
).rejects.toThrow("JWT verification failed: Token's app property is different than app ID."); ).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.");
});
}); });

View file

@ -1,21 +1,30 @@
import * as jose from "jose"; import * as jose from "jose";
import { createDebug } from "./debug"; import { createDebug } from "./debug";
import { hasPermissionsInJwtToken } from "./has-permissions-in-jwt-token";
import { AppPermission } from "./types";
import { getJwksUrlFromSaleorApiUrl } from "./urls"; import { getJwksUrlFromSaleorApiUrl } from "./urls";
const debug = createDebug("verify-jwt"); const debug = createDebug("verify-jwt");
export interface DashboardTokenPayload extends jose.JWTPayload { export interface DashboardTokenPayload extends jose.JWTPayload {
app: string; app: string;
user_permissions: AppPermission[];
} }
export interface verifyJWTArguments { export interface verifyJWTArguments {
appId: string; appId: string;
saleorApiUrl: string; saleorApiUrl: string;
token: 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; let tokenClaims: DashboardTokenPayload;
const ERROR_MESSAGE = "JWT verification failed:"; 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.`); 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 { try {
debug("Trying to create JWKS"); debug("Trying to create JWKS");