@guardian/pan-domain-node
Version:
NodeJs implementation of Guardian pan-domain auth verification
299 lines (298 loc) • 17.1 kB
JavaScript
;
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);
}));
});
});