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 { 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");
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
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 })
|
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.");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue