Add EXP field validation in JWT (#233)

* Add token exp validaiton

* revert lock
This commit is contained in:
Lukasz Ostrowski 2023-04-26 14:09:50 +02:00 committed by GitHub
parent 463fffb66c
commit da351faae9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 125 additions and 1 deletions

View file

@ -0,0 +1,5 @@
---
"@saleor/app-sdk": minor
---
Added JWT token expire date validation (claims.exp field). If token is expired, error will be thrown and verification will be aborted

View file

@ -2,6 +2,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { verifyJWT } from "./verify-jwt"; import { verifyJWT } from "./verify-jwt";
/**
* exp field points to Nov 24, 2022
*/
const validToken = const validToken =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6Ijk4ZTEzNDk4YmM5NThjM2QyNzk2NjY5Zjk0NzYxMzZkIn0.eyJpYXQiOjE2NjkxOTE4NDUsIm93bmVyIjoic2FsZW9yIiwiaXNzIjoiZGVtby5ldS5zYWxlb3IuY2xvdWQiLCJleHAiOjE2NjkyNzgyNDUsInRva2VuIjoic2JsRmVrWnVCSUdXIiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsInR5cGUiOiJ0aGlyZHBhcnR5IiwidXNlcl9pZCI6IlZYTmxjam95TWc9PSIsImlzX3N0YWZmIjp0cnVlLCJhcHAiOiJRWEJ3T2pJM05RPT0iLCJwZXJtaXNzaW9ucyI6W10sInVzZXJfcGVybWlzc2lvbnMiOlsiTUFOQUdFX1BBR0VfVFlQRVNfQU5EX0FUVFJJQlVURVMiLCJNQU5BR0VfUFJPRFVDVF9UWVBFU19BTkRfQVRUUklCVVRFUyIsIk1BTkFHRV9ESVNDT1VOVFMiLCJNQU5BR0VfUExVR0lOUyIsIk1BTkFHRV9TVEFGRiIsIk1BTkFHRV9QUk9EVUNUUyIsIk1BTkFHRV9TSElQUElORyIsIk1BTkFHRV9UUkFOU0xBVElPTlMiLCJNQU5BR0VfT0JTRVJWQUJJTElUWSIsIk1BTkFHRV9VU0VSUyIsIk1BTkFHRV9BUFBTIiwiTUFOQUdFX0NIQU5ORUxTIiwiTUFOQUdFX0dJRlRfQ0FSRCIsIkhBTkRMRV9QQVlNRU5UUyIsIklNUEVSU09OQVRFX1VTRVIiLCJNQU5BR0VfU0VUVElOR1MiLCJNQU5BR0VfUEFHRVMiLCJNQU5BR0VfTUVOVVMiLCJNQU5BR0VfQ0hFQ0tPVVRTIiwiSEFORExFX0NIRUNLT1VUUyIsIk1BTkFHRV9PUkRFUlMiXX0.PUyvuUlDvUBXMGSaexusdlkY5wF83M8tsjefVXOknaKuVgLbafvLOgx78YGVB4kdAybC7O3Yjs7IIFOzz5U80Q"; "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6Ijk4ZTEzNDk4YmM5NThjM2QyNzk2NjY5Zjk0NzYxMzZkIn0.eyJpYXQiOjE2NjkxOTE4NDUsIm93bmVyIjoic2FsZW9yIiwiaXNzIjoiZGVtby5ldS5zYWxlb3IuY2xvdWQiLCJleHAiOjE2NjkyNzgyNDUsInRva2VuIjoic2JsRmVrWnVCSUdXIiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsInR5cGUiOiJ0aGlyZHBhcnR5IiwidXNlcl9pZCI6IlZYTmxjam95TWc9PSIsImlzX3N0YWZmIjp0cnVlLCJhcHAiOiJRWEJ3T2pJM05RPT0iLCJwZXJtaXNzaW9ucyI6W10sInVzZXJfcGVybWlzc2lvbnMiOlsiTUFOQUdFX1BBR0VfVFlQRVNfQU5EX0FUVFJJQlVURVMiLCJNQU5BR0VfUFJPRFVDVF9UWVBFU19BTkRfQVRUUklCVVRFUyIsIk1BTkFHRV9ESVNDT1VOVFMiLCJNQU5BR0VfUExVR0lOUyIsIk1BTkFHRV9TVEFGRiIsIk1BTkFHRV9QUk9EVUNUUyIsIk1BTkFHRV9TSElQUElORyIsIk1BTkFHRV9UUkFOU0xBVElPTlMiLCJNQU5BR0VfT0JTRVJWQUJJTElUWSIsIk1BTkFHRV9VU0VSUyIsIk1BTkFHRV9BUFBTIiwiTUFOQUdFX0NIQU5ORUxTIiwiTUFOQUdFX0dJRlRfQ0FSRCIsIkhBTkRMRV9QQVlNRU5UUyIsIklNUEVSU09OQVRFX1VTRVIiLCJNQU5BR0VfU0VUVElOR1MiLCJNQU5BR0VfUEFHRVMiLCJNQU5BR0VfTUVOVVMiLCJNQU5BR0VfQ0hFQ0tPVVRTIiwiSEFORExFX0NIRUNLT1VUUyIsIk1BTkFHRV9PUkRFUlMiXX0.PUyvuUlDvUBXMGSaexusdlkY5wF83M8tsjefVXOknaKuVgLbafvLOgx78YGVB4kdAybC7O3Yjs7IIFOzz5U80Q";
@ -9,8 +12,14 @@ const validApiUrl = "https://demo.eu.saleor.cloud/graphql/";
const validAppId = "QXBwOjI3NQ=="; const validAppId = "QXBwOjI3NQ==";
const mockedTodayDateBeforeTokenExp = new Date(2022, 10, 20);
const mockedTodayDateAfterTokenExp = new Date(2022, 10, 26);
describe("verifyJWT", () => { describe("verifyJWT", () => {
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(mockedTodayDateBeforeTokenExp);
vi.mock("jose", async () => { vi.mock("jose", async () => {
const original = await vi.importActual("jose"); const original = await vi.importActual("jose");
return { return {
@ -24,6 +33,7 @@ describe("verifyJWT", () => {
afterEach(() => { afterEach(() => {
vi.restoreAllMocks(); vi.restoreAllMocks();
vi.useRealTimers();
}); });
it("Process valid request", async () => { it("Process valid request", async () => {
@ -52,4 +62,16 @@ describe("verifyJWT", () => {
}) })
).rejects.toThrow("JWT verification failed: Token's permissions are not sufficient."); ).rejects.toThrow("JWT verification failed: Token's permissions are not sufficient.");
}); });
it("Throws if today date is newer than token expiration", async () => {
vi.setSystemTime(mockedTodayDateAfterTokenExp);
await expect(
verifyJWT({
appId: validAppId,
saleorApiUrl: validApiUrl,
token: validToken,
})
).rejects.toThrow("JWT verification failed: Token is expired");
});
}); });

View file

@ -4,6 +4,7 @@ import { createDebug } from "./debug";
import { hasPermissionsInJwtToken } from "./has-permissions-in-jwt-token"; import { hasPermissionsInJwtToken } from "./has-permissions-in-jwt-token";
import { Permission } from "./types"; import { Permission } from "./types";
import { getJwksUrlFromSaleorApiUrl } from "./urls"; import { getJwksUrlFromSaleorApiUrl } from "./urls";
import { verifyTokenExpiration } from "./verify-token-expiration";
const debug = createDebug("verify-jwt"); const debug = createDebug("verify-jwt");
@ -36,6 +37,12 @@ export const verifyJWT = async ({
throw new Error(`${ERROR_MESSAGE} Could not decode authorization token.`); throw new Error(`${ERROR_MESSAGE} Could not decode authorization token.`);
} }
try {
verifyTokenExpiration(tokenClaims);
} catch (e) {
throw new Error(`${ERROR_MESSAGE} ${(e as Error).message}`);
}
if (tokenClaims.app !== appId) { if (tokenClaims.app !== appId) {
debug( debug(
"Resolved App ID value from token to be different than in request, will respond with Bad Request" "Resolved App ID value from token to be different than in request, will respond with Bad Request"

View file

@ -0,0 +1,61 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { DashboardTokenPayload } from "./verify-jwt";
import { verifyTokenExpiration } from "./verify-token-expiration";
describe("verifyTokenExpiration", () => {
const mockTodayDate = new Date(2020, 2, 1, 9, 0, 0);
beforeAll(() => {
vi.useFakeTimers();
});
beforeEach(() => {
vi.setSystemTime(mockTodayDate);
});
afterAll(() => {
vi.useRealTimers();
});
it("Passes if exp field in token is in the future from \"now\"", () => {
const tokenDate = new Date(2020, 2, 1, 12, 0, 0);
expect(() =>
verifyTokenExpiration({
/**
* Must be seconds
*/
exp: tokenDate.valueOf() / 1000,
} as DashboardTokenPayload)
).not.toThrow();
});
it("Throws if exp field is missing", () => {
expect(() => verifyTokenExpiration({} as DashboardTokenPayload)).toThrow();
});
it("Throws if exp field is older than today", () => {
const tokenDate = new Date(2020, 2, 1, 4, 0, 0);
expect(() =>
verifyTokenExpiration({
/**
* Must be seconds
*/
exp: tokenDate.valueOf() / 1000,
} as DashboardTokenPayload)
).toThrow();
});
it("Throws if exp field is the same as today", () => {
expect(() =>
verifyTokenExpiration({
/**
* Must be seconds
*/
exp: mockTodayDate.valueOf() / 1000,
} as DashboardTokenPayload)
).toThrow();
});
});

View file

@ -0,0 +1,29 @@
import { createDebug } from "./debug";
import { DashboardTokenPayload } from "./verify-jwt";
const debug = createDebug("verify-token-expiration");
export const verifyTokenExpiration = (token: DashboardTokenPayload) => {
const tokenExpiration = token.exp;
const now = new Date();
const nowTimestamp = now.valueOf();
if (!tokenExpiration) {
throw new Error("Missing \"exp\" field in token");
}
/**
* Timestamp in token are in seconds, but timestamp from Date is in miliseconds
*/
const tokenMsTimestamp = tokenExpiration * 1000;
debug(
"Comparing todays date: %s and token expiration date: %s",
now.toLocaleString(),
new Date(tokenMsTimestamp).toLocaleString()
);
if (tokenMsTimestamp <= nowTimestamp) {
throw new Error("Token is expired");
}
};

View file

@ -5,8 +5,8 @@ import { defineConfig } from "vitest/config";
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
test: { test: {
setupFiles: ["./src/setup-tests.ts"],
environment: "jsdom", environment: "jsdom",
setupFiles: "./src/setup-tests.ts",
css: false, css: false,
coverage: { coverage: {
provider: "c8", provider: "c8",