UNPKG

@guardian/pan-domain-node

Version:

NodeJs implementation of Guardian pan-domain auth verification

299 lines (298 loc) 17.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const api_1 = require("../src/api"); const panda_1 = require("../src/panda"); const fetch_public_key_1 = require("../src/fetch-public-key"); const fixtures_1 = require("./fixtures"); const utils_1 = require("../src/utils"); jest.mock('../src/fetch-public-key'); jest.useFakeTimers(); function userFromCookie(cookie) { // This function is only used to generate a `User` object from // a well-formed text fixture cookie, in order to check that successful // `AuthenticationResult`s have the right shape. As such we don't want // to have to deal with the case of a bad cookie so we just cast to `ParsedCookie`. const parsedCookie = (0, utils_1.parseCookie)(cookie); return (0, utils_1.parseUser)(parsedCookie.data); } describe('verifyUser', function () { test("fail to authenticate if cookie is missing", () => { const expected = { success: false, reason: 'no-cookie' }; expect((0, panda_1.verifyUser)(undefined, "", new Date(0), api_1.guardianValidation)).toStrictEqual(expected); }); test("fail to authenticate if signature is malformed", () => { const [data, signature] = fixtures_1.sampleCookie.split("."); const testCookie = data + ".1234"; const expected = { success: false, reason: 'invalid-cookie' }; expect((0, panda_1.verifyUser)(testCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected); }); test("fail to authenticate if cookie expired and we're outside the grace period", () => { // Cookie expires at epoch time 1234 const afterEndOfGracePeriod = new Date(1234 + api_1.gracePeriodInMillis + 1); const expected = { success: false, reason: 'expired-cookie' }; expect((0, panda_1.verifyUser)(fixtures_1.sampleCookie, fixtures_1.publicKey, afterEndOfGracePeriod, api_1.guardianValidation)).toStrictEqual(expected); }); test("fail to authenticate if user fails validation function", () => { expect((0, panda_1.verifyUser)(fixtures_1.sampleCookieWithoutMultifactor, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual({ success: false, reason: 'invalid-user', user: userFromCookie(fixtures_1.sampleCookieWithoutMultifactor) }); expect((0, panda_1.verifyUser)(fixtures_1.sampleNonGuardianCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual({ success: false, reason: 'invalid-user', user: userFromCookie(fixtures_1.sampleNonGuardianCookie) }); }); test("fail to authenticate with invalid-cookie reason if signature is not valid", () => { const expected = { success: false, reason: 'invalid-cookie' }; const slightlyBadCookie = fixtures_1.sampleCookie.slice(0, -2); expect((0, panda_1.verifyUser)(slightlyBadCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected); }); test("fail to authenticate with invalid-cookie reason if data part is not base64", () => { const expected = { success: false, reason: 'invalid-cookie' }; const [_, signature] = fixtures_1.sampleCookie.split("."); const nonBase64Data = "not-base64-data"; const testCookie = `${nonBase64Data}.${signature}`; expect((0, panda_1.verifyUser)(testCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected); }); test("fail to authenticate with invalid-cookie reason if signature part is not base64", () => { const expected = { success: false, reason: 'invalid-cookie' }; const [data, _] = fixtures_1.sampleCookie.split("."); const nonBase64Signature = "not-base64-signature"; const testCookie = `${data}.${nonBase64Signature}`; expect((0, panda_1.verifyUser)(testCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected); }); test("fail to authenticate with invalid-cookie reason if cookie has no dot separator", () => { const expected = { success: false, reason: 'invalid-cookie' }; const noDotCookie = fixtures_1.sampleCookie.replace(".", ""); expect((0, panda_1.verifyUser)(noDotCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected); }); test("fail to authenticate with invalid-cookie reason if cookie has multiple dot separators", () => { const expected = { success: false, reason: 'invalid-cookie' }; const multipleDotsCookie = fixtures_1.sampleCookie.replace(".", ".."); expect((0, panda_1.verifyUser)(multipleDotsCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected); }); test("authenticate if the cookie and user are valid", () => { const expected = { success: true, // Cookie is not expired so no need to refresh credentials shouldRefreshCredentials: false, user: userFromCookie(fixtures_1.sampleCookie) }; expect((0, panda_1.verifyUser)(fixtures_1.sampleCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected); }); test("authenticate with shouldRefreshCredentials if cookie expired but we're within the grace period", () => { const beforeEndOfGracePeriod = new Date(1234 + api_1.gracePeriodInMillis - 1); const expected = { success: true, user: userFromCookie(fixtures_1.sampleCookie), shouldRefreshCredentials: true, mustRefreshByEpochTimeMillis: 1234 + api_1.gracePeriodInMillis }; expect((0, panda_1.verifyUser)(fixtures_1.sampleCookie, fixtures_1.publicKey, beforeEndOfGracePeriod, api_1.guardianValidation)).toStrictEqual(expected); }); }); describe('createCookie', function () { it('should return the same cookie based on the user details being provided', function () { const user = { firstName: "Test", lastName: "User", email: "test.user@guardian.co.uk", authenticatingSystem: "test", authenticatedIn: ["test"], expires: 1234, multifactor: true }; const cookie = (0, panda_1.createCookie)(user, fixtures_1.privateKey); expect((0, utils_1.decodeBase64)(cookie)).toEqual((0, utils_1.decodeBase64)(fixtures_1.sampleCookie)); expect(cookie).toEqual(fixtures_1.sampleCookie); }); }); describe('panda class', function () { beforeEach(() => { fetch_public_key_1.fetchPublicKey.mockResolvedValue({ key: 'PUBLIC KEY', lastUpdated: new Date() }); }); describe('stop', () => { it('stops auto refresh', () => { const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u) => true); expect(panda.keyUpdateTimer).not.toBeUndefined(); panda.stop(); expect(panda.keyUpdateTimer).toBeUndefined(); }); }); describe('getPublicKey', () => { it('getsPublicKey immediately when last fetch is within the cache time', () => __awaiter(this, void 0, void 0, function* () { const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u) => true); const fetchesBeforeGet = fetch_public_key_1.fetchPublicKey.mock.calls.length; yield expect(panda.getPublicKey()).resolves.toEqual('PUBLIC KEY'); const fetchesAfterGet = fetch_public_key_1.fetchPublicKey.mock.calls.length; expect(fetchesAfterGet).toEqual(fetchesBeforeGet); })); it('getsPublicKey after refetching when last fetch is outside the cache time', () => __awaiter(this, void 0, void 0, function* () { // cache time is 1 min const fiveMinsAgo = new Date(); fiveMinsAgo.setMinutes(fiveMinsAgo.getMinutes() - 5); fetch_public_key_1.fetchPublicKey.mockResolvedValue({ key: 'PUBLIC KEY', lastUpdated: fiveMinsAgo }); const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u) => true); const fetchesBefore = fetch_public_key_1.fetchPublicKey.mock.calls.length; yield expect(panda.getPublicKey()).resolves.toEqual('PUBLIC KEY'); fetch_public_key_1.fetchPublicKey.mockResolvedValue({ key: 'PUBLIC KEY 2', lastUpdated: fiveMinsAgo }); const fetchesAfter = fetch_public_key_1.fetchPublicKey.mock.calls.length; yield expect(panda.getPublicKey()).resolves.toEqual('PUBLIC KEY 2'); expect(fetchesAfter).toEqual(fetchesBefore + 1); })); }); describe('verify', () => { beforeEach(() => { fetch_public_key_1.fetchPublicKey.mockResolvedValue({ key: fixtures_1.publicKey, lastUpdated: new Date() }); }); it('should authenticate if cookie and user are valid', () => __awaiter(this, void 0, void 0, function* () { jest.setSystemTime(100); const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u) => true); const authenticationResult = yield panda.verify(`cookiename=${fixtures_1.sampleCookie}`); const expected = { success: true, // Cookie is not expired shouldRefreshCredentials: false, user: userFromCookie(fixtures_1.sampleCookie) }; expect(authenticationResult).toStrictEqual(expected); })); it('should authenticate if cookie and user are valid when multiple cookies are passed', () => __awaiter(this, void 0, void 0, function* () { jest.setSystemTime(100); const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u) => true); const authenticationResult = yield panda.verify(`a=blah; b=stuff; cookiename=${fixtures_1.sampleCookie}; c=4958345`); const expected = { success: true, // Cookie is not expired shouldRefreshCredentials: false, user: userFromCookie(fixtures_1.sampleCookie) }; expect(authenticationResult).toStrictEqual(expected); })); it('should fail to authenticate if cookie expired and we\'re outside the grace period', () => __awaiter(this, void 0, void 0, function* () { // Cookie expiry is 1234 const afterEndOfGracePeriodEpochMillis = 1234 + api_1.gracePeriodInMillis + 1; jest.setSystemTime(afterEndOfGracePeriodEpochMillis); const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u) => true); const authenticationResult = yield panda.verify(`cookiename=${fixtures_1.sampleCookie}`); const expected = { success: false, reason: 'expired-cookie' }; expect(authenticationResult).toStrictEqual(expected); })); it('authenticate with shouldRefreshCredentials if cookie expired but we\'re within the grace period', () => __awaiter(this, void 0, void 0, function* () { // Cookie expiry is 1234 const beforeEndOfGracePeriodEpochMillis = 1234 + api_1.gracePeriodInMillis - 1; jest.setSystemTime(beforeEndOfGracePeriodEpochMillis); const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u) => true); const authenticationResult = yield panda.verify(`cookiename=${fixtures_1.sampleCookie}`); const expected = { success: true, shouldRefreshCredentials: true, mustRefreshByEpochTimeMillis: 1234 + api_1.gracePeriodInMillis, user: userFromCookie(fixtures_1.sampleCookie) }; expect(authenticationResult).toStrictEqual(expected); })); it('should fail to authenticate if user is not valid', () => __awaiter(this, void 0, void 0, function* () { jest.setSystemTime(100); const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', api_1.guardianValidation); const authenticationResult = yield panda.verify(`cookiename=${fixtures_1.sampleNonGuardianCookie}`); const expected = { success: false, reason: 'invalid-user', user: userFromCookie(fixtures_1.sampleNonGuardianCookie) }; expect(authenticationResult).toStrictEqual(expected); })); it('should fail to authenticate if there is no cookie with the correct name', () => __awaiter(this, void 0, void 0, function* () { jest.setSystemTime(100); const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', api_1.guardianValidation); const authenticationResult = yield panda.verify(`wrongcookiename=${fixtures_1.sampleNonGuardianCookie}`); const expected = { success: false, reason: "no-cookie" }; expect(authenticationResult).toStrictEqual(expected); })); it('should fail to authenticate if the cookie request header is malformed', () => __awaiter(this, void 0, void 0, function* () { jest.setSystemTime(100); const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', api_1.guardianValidation); // The cookie headers should be semicolon-separated name=valueg const authenticationResult = yield panda.verify(fixtures_1.sampleNonGuardianCookie); const expected = { success: false, reason: "no-cookie" }; expect(authenticationResult).toStrictEqual(expected); })); it('should fail to authenticate if there is no cookie with the correct name out of multiple cookies', () => __awaiter(this, void 0, void 0, function* () { jest.setSystemTime(100); const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', api_1.guardianValidation); const authenticationResult = yield panda.verify(`wrongcookiename=${fixtures_1.sampleNonGuardianCookie}; anotherwrongcookiename=${fixtures_1.sampleNonGuardianCookie}`); const expected = { success: false, reason: "no-cookie" }; expect(authenticationResult).toStrictEqual(expected); })); it('should fail to authenticate with invalid-cookie reason if cookie is malformed', () => __awaiter(this, void 0, void 0, function* () { jest.setSystemTime(100); const panda = new panda_1.PanDomainAuthentication('rightcookiename', 'region', 'bucket', 'keyfile', api_1.guardianValidation); // There is a valid Panda cookie in here, but it's under the wrong name const authenticationResult = yield panda.verify(`wrongcookiename=${fixtures_1.sampleNonGuardianCookie}; rightcookiename=not-valid-panda-cookie`); const expected = { success: false, reason: "invalid-cookie" }; expect(authenticationResult).toStrictEqual(expected); })); it('should fail to authenticate with no-cookie reason if no cookie is present at all', () => __awaiter(this, void 0, void 0, function* () { jest.setSystemTime(100); const panda = new panda_1.PanDomainAuthentication('rightcookiename', 'region', 'bucket', 'keyfile', api_1.guardianValidation); const noCookie = undefined; const authenticationResult = yield panda.verify(noCookie); const expected = { success: false, reason: "no-cookie" }; expect(authenticationResult).toStrictEqual(expected); })); }); });