Skip to content

Commit

Permalink
Merge pull request #53 from icapps/feature/AE-1987-sso-authentication
Browse files Browse the repository at this point in the history
feature/AE-1987-sso-authentication
  • Loading branch information
horstenwillem committed Apr 3, 2020
2 parents 13fd1a4 + 9b8a076 commit 9b3d923
Show file tree
Hide file tree
Showing 9 changed files with 4,082 additions and 2,470 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Expand Up @@ -2,7 +2,6 @@
language: node_js

node_js:
- "8"
- "10"
- "12"

Expand Down
6,373 changes: 3,927 additions & 2,446 deletions package-lock.json

Large diffs are not rendered by default.

28 changes: 15 additions & 13 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "@icapps/tree-house-authentication",
"version": "3.1.5",
"version": "3.1.6",
"description": "Tree House Authentication module",
"main": "build/index.js",
"types": "build/index.d.ts",
Expand All @@ -21,35 +21,37 @@
"lint"
],
"dependencies": {
"bcrypt": "~3.0.7",
"bcrypt": "~3.0.8",
"express-session": "~1.17.0",
"jsonwebtoken": "~8.5.1",
"jwks-rsa": "~1.7.0",
"ldapjs": "~1.0.2",
"openid-client": "~3.14.1",
"qrcode": "~1.4.4",
"samlify": "~2.6.2",
"samlify": "~2.7.1",
"speakeasy": "~2.0.0"
},
"devDependencies": {
"@types/bcrypt": "~3.0.0",
"@types/express-session": "~1.15.16",
"@types/jest": "~24.9.0",
"@types/jsonwebtoken": "~8.3.6",
"@types/ldapjs": "~1.0.4",
"@types/express-session": "~1.17.0",
"@types/jest": "~25.1.5",
"@types/jsonwebtoken": "~8.3.8",
"@types/ldapjs": "~1.0.6",
"@types/qrcode": "~1.3.4",
"@types/speakeasy": "~2.0.5",
"coveralls": "~3.0.9",
"jest": "~24.9.0",
"coveralls": "~3.0.11",
"jest": "~25.2.7",
"node-mocks-http": "~1.8.1",
"np": "~5.2.1",
"pre-commit": "~1.2.2",
"supertest": "~4.0.2",
"ts-jest": "~24.3.0",
"ts-jest": "~25.3.0",
"tslint-config-airbnb": "~5.11.2",
"tslint": "~5.20.1",
"typescript": "~3.7.5"
"tslint": "~6.1.1",
"typescript": "~3.8.3"
},
"engines": {
"node": ">=8.0.0"
"node": ">=10.13.0"
},
"directories": {
"test": "test"
Expand Down
5 changes: 5 additions & 0 deletions src/config/jwt.config.ts
Expand Up @@ -7,3 +7,8 @@ export const DEFAULT_JWT_CONFIG = {
issuer: 'treehouse-authentication',
secretOrKey: '5kZxE|gZu1ODB183s772)/3:l_#5hU3Gn5O|2ux3&lhN@LQ6g+"i$zqB_C<6',
};

export const DEFAULT_JWT_DECODE_OPTIONS = {
complete: false,
json: false,
};
1 change: 1 addition & 0 deletions src/index.ts
@@ -1,5 +1,6 @@
import * as saml from './lib/saml-authentication';

export * from './lib/sso-authentication';
export * from './lib/jwt-authentication';
export * from './lib/session-authentication';
export * from './lib/two-factor-authentication';
Expand Down
10 changes: 5 additions & 5 deletions src/lib/jwt-authentication.ts
@@ -1,5 +1,5 @@
import { sign as jwtSign, verify as jwtVerify, decode as jwtDecode, Secret, SignOptions } from 'jsonwebtoken';
import { DEFAULT_JWT_CONFIG } from '../config/jwt.config';
import { sign as jwtSign, verify as jwtVerify, decode as jwtDecode, Secret, SignOptions, DecodeOptions } from 'jsonwebtoken';
import { DEFAULT_JWT_CONFIG, DEFAULT_JWT_DECODE_OPTIONS } from '../config/jwt.config';

/**
* Create a JWT token
Expand All @@ -21,8 +21,8 @@ export function authenticateJwt(token: string, options: CustomSignOptions = DEFA
/**
* Decode a json webtoken without validation
*/
export function decodeJwt(token: string): null | object | string {
return jwtDecode(token);
export function decodeJwt(token: string, options: DecodeOptions = DEFAULT_JWT_DECODE_OPTIONS): null | { [key: string]: any } | string {
return jwtDecode(token, options);
}

/**
Expand All @@ -40,7 +40,7 @@ function signJwt(payload: Object, secretOrKey: Secret, jwtSettings: SignOptions)
/**
* Verify whether the provided jwt token is valid and return decoded information
*/
function verifyJwt(token: string, secretOrKey: string | Buffer, jwtSettings: SignOptions): Promise<{}> {
export function verifyJwt(token: string, secretOrKey: string | Buffer, jwtSettings: SignOptions): Promise<{}> {
return new Promise((resolve, reject) => {
jwtVerify(token, secretOrKey, jwtSettings, (error, decoded) => {
if (error) reject(`Something went wrong trying to verify the json webtoken. Actual error: ${error}`);
Expand Down
70 changes: 70 additions & 0 deletions src/lib/sso-authentication.ts
@@ -0,0 +1,70 @@
import { Issuer } from 'openid-client';
import jwksClient = require('jwks-rsa');
import { Algorithm } from 'jsonwebtoken';

import { verifyJwt, decodeJwt } from './jwt-authentication';

/**
* Authenticate whether the provided SSO token is valid
* @param {String} token
* @returns {Object}
*/
export async function authenticateSso(token: string): Promise<{}> {
if (token === '') throw new Error('SSO token is empty.');

const { header, payload } = decodeJwt(token, { complete: true }) as CompleteJWTToken;
const { metadata } = await Issuer.discover(payload.iss);

const secret = await getKey(metadata.jwks_uri, header.kid);

const options = {
issuer: payload.iss,
algorithm: header.alg,
aud: payload.aud,
expiresIn: payload.exp,
};

return verifyJwt(token, secret, options);
}

/**
* Get key from a JSON Web Key Set endpoint
* @param {String} jwksUri
* @param {String} token
* @returns {String}
*/
export async function getKey(jwksUri: string, token: string): Promise<string> {
const client = jwksClient({
jwksUri,
});

return new Promise((resolve, reject) => {
client.getSigningKey(token, (error, key: any) => {
if (error) return reject(error);

const signingKey = key.rsaPublicKey || key.publicKey;
return resolve(signingKey);
});
});
}

interface CompleteJWTToken {
header: {
typ: string,
kid: string,
alg: Algorithm,
};
payload: {
sub: string,
iss: string,
tokenName: string,
nonce: string,
aud: string,
azp: string,
auth_time: number,
realm: string,
exp: number,
tokenType: string,
iat: number,
};
}
10 changes: 5 additions & 5 deletions tests/ldap-authentication.test.ts
Expand Up @@ -79,7 +79,7 @@ describe('ldap-authentication', () => {
};

const dnString = 'dc=example,dc=com';
const filter = {
const filter: ldap.SearchOptions = {
filter: '(objectClass=*)',
scope: 'sub',
};
Expand All @@ -91,15 +91,15 @@ describe('ldap-authentication', () => {
setTimeout(() => {
emitter.emit('searchEntry', entry);
emitter.emit('end', 'ok');
}, 200);
}, 200);

const users = await searchUsers(client, dnString, filter);
expect(users).toContainEqual(expectedToFind);
});

it('Should return an error on bad searchrequest', async () => {
const dnString = '';
const filter = {
const filter: ldap.SearchOptions = {
scope: 'sub',
};

Expand All @@ -108,7 +108,7 @@ describe('ldap-authentication', () => {

setTimeout(() => {
emitter.emit('error', new Error('Bad search request'));
}, 400);
}, 400);

expect.assertions(1);
try {
Expand All @@ -120,7 +120,7 @@ describe('ldap-authentication', () => {

it('Should return an error when it cannot get a search result', async () => {
const dnString = '';
const filter = {
const filter: ldap.SearchOptions = {
scope: 'sub',
};

Expand Down
54 changes: 54 additions & 0 deletions tests/sso-authentication.test.ts
@@ -0,0 +1,54 @@
import { Algorithm } from 'jsonwebtoken';
import jwksClient = require('jwks-rsa');

import { createJwt, getKey } from '../src';

const validJwtConfiguration = {
algorithm: 'HS256' as Algorithm,
expiresIn: '10m',
audience: 'TREEHOUSE-AUTH',
issuer: 'https://test.com/id/oauth2',
secretOrKey: '5kZxE|gZu1ODB183s772)/3:l_#5hU3Gn5O|2ux3&lhN@LQ6g+"i$zqB_C<6',
};

// Mock jwks-rsa module
jest.genMockFromModule('jwks-rsa');
jest.mock('jwks-rsa');

const mockJwksClient = {
getSigningKey: jest.fn((_token, callbackFn) => callbackFn(null, null)),
getKeys: jest.fn(),
getSigningKeys: jest.fn(),
};

(jwksClient as unknown as jest.Mock).mockImplementation(() => mockJwksClient);

let token: any = null;

describe('#SSO authentication', () => {
beforeAll(async () => {
token = await createJwt({ sub: 'FakeUser' }, validJwtConfiguration);
});

describe('getKey', () => {
it('Should return the siging key of the token', async () => {
mockJwksClient.getSigningKey.mockImplementation((_token, callBackFn) => callBackFn(null, { rsaPublicKey: 'secret' }));
const secret = await getKey(validJwtConfiguration.issuer, token);
expect(secret).toEqual('secret');
});

it('Should throw an error', async () => {
mockJwksClient.getSigningKey.mockImplementation((_token, callBackFn) => callBackFn(new Error('Something went wrong')));
try {
await getKey(validJwtConfiguration.issuer, token);
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(error.message).toEqual('Something went wrong');
}
});
});

describe('authenticateSso', () => {
// TODO
});
});

0 comments on commit 9b3d923

Please sign in to comment.