keycloak-connect-multirealm
Version:
Keycloak Node.js Adapter With Support for multiple realms
489 lines (409 loc) • 17.4 kB
JavaScript
const Keycloak = require('keycloak-connect');
const jwt = require('jsonwebtoken');
const NodeCache = require('node-cache');
const composable = require('composable-middleware');
jest.mock('../node_modules/keycloak-connect/middleware/admin');
jest.mock('../node_modules/keycloak-connect/middleware/logout');
jest.mock('../node_modules/keycloak-connect/middleware/post-auth');
jest.mock('../node_modules/keycloak-connect/middleware/grant-attacher');
jest.mock('../node_modules/keycloak-connect/middleware/protect');
const Admin = require('../node_modules/keycloak-connect/middleware/admin');
const Logout = require('../node_modules/keycloak-connect/middleware/logout');
const PostAuth = require('../node_modules/keycloak-connect/middleware/post-auth');
const GrantAttacher = require('../node_modules/keycloak-connect/middleware/grant-attacher');
const Protect = require('../node_modules/keycloak-connect/middleware/protect');
const KeycloakMultiRealm = require('./KeycloakMultiRealm');
const keycloakJsonFile = require('../keycloak');
const anotherKeycloakJsonFile = require('../another-keycloak');
describe('KeycloakMultiRealm', () => {
let keycloakMultiRealm, config, keycloakConfig, req, res, next, userId, keycloakMock, cache, composedMiddleware,
postAuthMiddleware, adminMiddleware, logoutMiddleware, grantAttacherMiddleware, protectMiddleware;
// eslint-disable-next-line max-statements
beforeEach(() => {
config = {};
keycloakConfig = {
'auth-server-url': 'http://localhost:8080/auth',
'bearer-only': true,
'ssl-required': 'external',
'resource': 'my-node-app',
};
req = {'obj': 'req', 'headers': {}, 'url': 'url'};
res = {
'obj': 'res',
'status': jest.fn(() => {
return res;
}),
'send': jest.fn(() => {
return res;
}),
};
next = jest.fn();
userId = 'me';
keycloakMock = new Keycloak();
keycloakMock.constructorCount = 0;
delete keycloakMock.constructorArguments;
keycloakMultiRealm = new KeycloakMultiRealm(config, keycloakConfig);
cache = new NodeCache({'useClones': false});
cache.get = jest.fn();
cache.get.mockClear();
cache.set.mockClear();
composedMiddleware = composable();
composable.mockClear();
composedMiddleware.mockClear();
postAuthMiddleware = jest.fn();
PostAuth.mockReturnValue(postAuthMiddleware);
PostAuth.mockClear();
adminMiddleware = jest.fn();
Admin.mockReturnValue(adminMiddleware);
Admin.mockClear();
logoutMiddleware = jest.fn();
Logout.mockReturnValue(logoutMiddleware);
Logout.mockClear();
grantAttacherMiddleware = jest.fn();
GrantAttacher.mockReturnValue(grantAttacherMiddleware);
GrantAttacher.mockClear();
protectMiddleware = jest.fn();
Protect.mockReturnValue(protectMiddleware);
Protect.mockClear();
});
describe('constructor', () => {
describe('when no config provided', () => {
it('should fail (just to maintain consistency with the official module behavior)', () => {
expect(() => {
keycloakMultiRealm = new KeycloakMultiRealm(null, keycloakConfig);
}).toThrow('Adapter configuration must be provided.');
});
});
describe('when no keycloakConfig provided', () => {
beforeEach(() => {
keycloakMultiRealm = new KeycloakMultiRealm(config);
});
it('should read config from keycloak.json', () => {
expect(keycloakMultiRealm.keycloakConfig).toEqual(keycloakJsonFile);
});
});
describe('keycloakConfig is a path string', () => {
beforeEach(() => {
keycloakMultiRealm = new KeycloakMultiRealm(config, 'another-keycloak.json');
});
it('should get config from the given path', () => {
expect(keycloakMultiRealm.keycloakConfig).toEqual(anotherKeycloakJsonFile);
});
});
});
describe('middleware()', () => {
it('should return a middleware function', () => {
expect(keycloakMultiRealm.middleware()).toEqual(expect.any(Function));
});
describe('middleware function', () => {
let middleware;
beforeEach(() => {
middleware = keycloakMultiRealm.middleware();
});
const runCommonTests = () => {
it('should call next once, without error', () => {
middleware(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith();
});
describe('no realm found', () => {
beforeEach(() => {
keycloakMultiRealm.getRealmNameFromRequest = jest.fn().mockReturnValue();
keycloakMultiRealm.getRealmNameFromToken = jest.fn().mockReturnValue();
});
it('should not set req.kauth object', () => {
middleware(req, res, next);
expect(req.kauth).toBeUndefined();
});
});
describe('realm found', () => {
const realm = 'devsu';
beforeEach(() => {
keycloakMultiRealm.getRealmNameFromRequest = jest.fn().mockReturnValue(realm);
});
describe('when keycloakObject not cached', () => {
it('should create a keycloak object and save it to cache', () => {
const expectedKeycloakConfig = Object.assign({}, keycloakConfig, {realm});
middleware(req, res, next);
expect(keycloakMock.constructorCount).toEqual(1);
expect(keycloakMock.constructorArguments).toEqual([config, expectedKeycloakConfig]);
expect(cache.get).toHaveBeenCalledTimes(1);
expect(cache.get).toHaveBeenCalledWith(realm);
expect(cache.set).toHaveBeenCalledTimes(1);
expect(cache.set).toHaveBeenCalledWith(realm, keycloakMock);
});
it('should add the authenticated and deauthenticated callbacks to the keycloak object', () => {
middleware(req, res, next);
expect(keycloakMock.authenticated).toBe(keycloakMultiRealm.authenticated);
expect(keycloakMock.deauthenticated).toBe(keycloakMultiRealm.deauthenticated);
});
});
describe('when keycloakObject cached', () => {
let myInstance;
beforeEach(() => {
myInstance = {};
cache.get = jest.fn().mockReturnValue(myInstance);
});
it('should use keycloak object from cache', () => {
middleware(req, res, next);
expect(cache.get).toHaveBeenCalledTimes(1);
expect(cache.get).toHaveBeenCalledWith(realm);
expect(cache.set).not.toHaveBeenCalled();
});
});
it('should call keycloak middleware', () => {
middleware(req, res, next);
expect(composable).toHaveBeenCalledTimes(1);
expect(composable).toHaveBeenCalledWith(
postAuthMiddleware, adminMiddleware, grantAttacherMiddleware, logoutMiddleware,
);
expect(composedMiddleware).toHaveBeenCalledTimes(1);
expect(composedMiddleware).toHaveBeenCalledWith(req, res, next);
});
it('should get keycloak middleware using the right arguments', () => {
middleware(req, res, next);
expect(PostAuth).toHaveBeenCalledTimes(1);
expect(PostAuth).toHaveBeenCalledWith(keycloakMock);
expect(Admin).toHaveBeenCalledTimes(1);
expect(Admin).toHaveBeenCalledWith(keycloakMock, '/'); // default options.admin
expect(GrantAttacher).toHaveBeenCalledTimes(1);
expect(GrantAttacher).toHaveBeenCalledWith(keycloakMock);
expect(Logout).toHaveBeenCalledTimes(1);
expect(Logout).toHaveBeenCalledWith(keycloakMock, '/logout'); // default options.logout
});
describe('when no options passed', () => {
it('should use default options', () => {
middleware(req, res, next);
expect(Admin).toHaveBeenCalledWith(keycloakMock, '/'); // default options.admin
expect(Logout).toHaveBeenCalledWith(keycloakMock, '/logout'); // default options.logout
});
});
describe('when passing options.admin', () => {
it('should use given options.admin', () => {
middleware = keycloakMultiRealm.middleware({'admin': '/duck'});
middleware(req, res, next);
expect(Admin).toHaveBeenCalledWith(keycloakMock, '/duck');
expect(Logout).toHaveBeenCalledWith(keycloakMock, '/logout');
});
});
describe('when passing options.logout', () => {
it('should use given options.logout', () => {
middleware = keycloakMultiRealm.middleware({'logout': '/cat'});
middleware(req, res, next);
expect(Admin).toHaveBeenCalledWith(keycloakMock, '/');
expect(Logout).toHaveBeenCalledWith(keycloakMock, '/cat');
});
});
it('should set kauth.realm with the realm name', () => {
middleware(req, res, next);
expect(req.kauth.realm).toEqual(realm);
});
});
};
const runCommonTestsForRequestsWithoutTokenOrWithInvalidToken = () => {
it('should try to get the realm name using the getRealmNameFromRequest() method', () => {
keycloakMultiRealm.getRealmNameFromRequest = jest.fn().mockReturnValue();
keycloakMultiRealm.getRealmNameFromToken = jest.fn();
middleware(req, res, next);
expect(keycloakMultiRealm.getRealmNameFromRequest).toHaveBeenCalledTimes(1);
expect(keycloakMultiRealm.getRealmNameFromRequest).toHaveBeenCalledWith(req);
expect(keycloakMultiRealm.getRealmNameFromToken).not.toHaveBeenCalled();
});
};
const runCommonTestsForRequestWithValidToken = () => {
it('should try to get the realm name using the getRealmNameFromToken() method', () => {
keycloakMultiRealm.getRealmNameFromRequest = jest.fn().mockReturnValue();
keycloakMultiRealm.getRealmNameFromToken = jest.fn();
middleware(req, res, next);
expect(keycloakMultiRealm.getRealmNameFromToken).toHaveBeenCalledTimes(1);
expect(keycloakMultiRealm.getRealmNameFromRequest).not.toHaveBeenCalled();
});
};
describe('without token', () => {
runCommonTests();
runCommonTestsForRequestsWithoutTokenOrWithInvalidToken();
});
describe('with invalid token', () => {
describe('invalid iss', () => {
let invalidIss;
beforeEach(() => {
invalidIss = 'http://invalid.iss/auth/realms/anyRealm';
const payload = {
'sub': userId,
'iss': invalidIss,
};
const token = jwt.sign(payload, 'secret!');
const authorization = `Bearer ${token}`;
req.headers.Authorization = authorization;
});
runCommonTestsForRequestsWithoutTokenOrWithInvalidToken();
runCommonTests();
});
describe('invalid iss - 2', () => {
let invalidIss;
beforeEach(() => {
invalidIss = 'http://invalid.iss/auth/realms/anyRealm?securityHole=http://auth.sm.localhost/auth';
const payload = {
'sub': userId,
'iss': invalidIss,
};
const token = jwt.sign(payload, 'secret!');
const authorization = `Bearer ${token}`;
req.headers.Authorization = authorization;
});
runCommonTestsForRequestsWithoutTokenOrWithInvalidToken();
runCommonTests();
});
});
describe('with valid token', () => {
let serverUrl, payload, token, realm, iss;
beforeEach(() => {
serverUrl = 'http://localhost:8080/auth';
realm = 'devsu';
iss = `${serverUrl}/auth/realms/${realm}`;
payload = {
'sub': userId,
'iss': iss,
};
token = jwt.sign(payload, 'secret!');
});
describe('with bearer prefix', () => {
beforeEach(() => {
const authorization = `Bearer ${token}`;
req.headers.Authorization = authorization;
});
runCommonTests();
runCommonTestsForRequestWithValidToken();
});
describe('without bearer prefix', () => {
beforeEach(() => {
req.headers.Authorization = token;
});
runCommonTests();
runCommonTestsForRequestWithValidToken();
});
});
});
});
describe('getRealmNameFromToken()', () => {
let token;
beforeEach(() => {
token = {'payload': {'iss': 'http://localhost:8080/auth/realms/pepito'}};
});
it('should return the realm name from the given token', () => {
expect(keycloakMultiRealm.getRealmNameFromToken(token)).toEqual('pepito');
});
});
describe('getRealmNameFromRequest()', () => {
it('should return undefined (user should provide implementation)', () => {
expect(keycloakMultiRealm.getRealmNameFromRequest({})).toBeUndefined();
});
});
describe('protect()', () => {
it('should return a function', () => {
expect(keycloakMultiRealm.protect()).toEqual(expect.any(Function));
});
describe('protect middleware function', () => {
let protect, spec;
beforeEach(() => {
spec = 'whatever';
protect = keycloakMultiRealm.protect(spec);
});
const runCommonTestsForRequestWithoutTokenOrWithInvalidToken = () => {
beforeEach(() => {
keycloakMultiRealm.accessDenied = jest.fn();
});
it('should call accessDenied method', () => {
protect(req, res, next);
expect(keycloakMultiRealm.accessDenied).toHaveBeenCalledTimes(1);
expect(keycloakMultiRealm.accessDenied).toHaveBeenCalledWith(req, res);
});
it('should not call Protect method', () => {
protect(req, res, next);
expect(Protect).not.toHaveBeenCalled();
});
it('should not call next', () => {
protect(req, res, next);
expect(next).not.toHaveBeenCalled();
});
};
const runCommonTestsForRequestsWithValidToken = () => {
it('should call the protect method on the corresponding keycloak object, with the right arguments', () => {
protect(req, res, next);
expect(Protect).toHaveBeenCalledTimes(1);
expect(Protect).toHaveBeenCalledWith(keycloakMock, spec);
expect(protectMiddleware).toHaveBeenCalledTimes(1);
expect(protectMiddleware).toHaveBeenCalledWith(req, res, next);
});
};
describe('without token', () => {
runCommonTestsForRequestWithoutTokenOrWithInvalidToken();
});
describe('with invalid token', () => {
describe('invalid iss', () => {
let invalidIss;
beforeEach(() => {
invalidIss = 'http://invalid.iss/auth/realms/anyRealm';
const payload = {
'sub': userId,
'iss': invalidIss,
};
const token = jwt.sign(payload, 'secret!');
const authorization = `Bearer ${token}`;
req.headers.Authorization = authorization;
});
runCommonTestsForRequestWithoutTokenOrWithInvalidToken();
});
describe('invalid iss - 2', () => {
let invalidIss;
beforeEach(() => {
invalidIss = 'http://invalid.iss/auth/realms/anyRealm?securityHole=http://auth.sm.localhost/auth';
const payload = {
'sub': userId,
'iss': invalidIss,
};
const token = jwt.sign(payload, 'secret!');
const authorization = `Bearer ${token}`;
req.headers.Authorization = authorization;
});
runCommonTestsForRequestWithoutTokenOrWithInvalidToken();
});
});
describe('with valid token', () => {
let serverUrl, payload, token, realm, iss;
beforeEach(() => {
serverUrl = 'http://localhost:8080/auth';
realm = 'devsu';
iss = `${serverUrl}/auth/realms/${realm}`;
payload = {
'sub': userId,
'iss': iss,
};
token = jwt.sign(payload, 'secret!');
});
describe('with bearer prefix', () => {
beforeEach(() => {
const authorization = `Bearer ${token}`;
req.headers.Authorization = authorization;
});
runCommonTestsForRequestsWithValidToken();
});
describe('without bearer prefix', () => {
beforeEach(() => {
req.headers.Authorization = token;
});
runCommonTestsForRequestsWithValidToken();
});
});
});
});
describe('accessDenied()', () => {
it('should return 403 with "access denied" message', () => {
keycloakMultiRealm.accessDenied(req, res);
expect(res.status).toHaveBeenCalledTimes(1);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.send).toHaveBeenCalledTimes(1);
expect(res.send).toHaveBeenCalledWith('Access Denied');
});
});
});