Skip to content

Commit

Permalink
Swap out keycloak-connect for jsonwebtoken
Browse files Browse the repository at this point in the history
Implementation taken from COMS
  • Loading branch information
norrisng-bc committed Apr 17, 2024
1 parent 81d9dff commit 2472018
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 34 deletions.
6 changes: 2 additions & 4 deletions app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ const helmet = require('helmet');

const { name: appName, version: appVersion } = require('./package.json');
const carboneCopyApi = require('./src/components/carboneCopyApi');
const keycloak = require('./src/components/keycloak');
const log = require('./src/components/log')(module.filename);
const httpLogger = require('./src/components/log').httpLogger;
const { getGitRevision, prettyStringify } = require('./src/components/utils');
const { getConfigBoolean, getGitRevision, prettyStringify } = require('./src/components/utils');
const v2Router = require('./src/routes/v2');

const { authorizedParty } = require('./src/middleware/authorizedParty');
Expand Down Expand Up @@ -52,9 +51,8 @@ if (process.env.NODE_ENV !== 'test') {
}

// Use Keycloak OIDC Middleware
if (config.has('keycloak.enabled')) {
if (getConfigBoolean('keycloak.enabled')) {
log.info('Running in authenticated mode');
app.use(keycloak.middleware());
} else {
log.info('Running in public mode');
}
Expand Down
16 changes: 0 additions & 16 deletions app/src/components/keycloak.js

This file was deleted.

40 changes: 40 additions & 0 deletions app/src/components/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const config = require('config');
const { existsSync, readFileSync } = require('fs');
const { join } = require('path');
const { v4: uuidv4 } = require('uuid');
Expand Down Expand Up @@ -39,6 +40,31 @@ module.exports = {
return `${name}.${extension}`;
},

/**
* @function getConfigBoolean
* Gets the value of a boolean node-config key.
* Keys that don't exist in the config are automatically converted to `false`,
* thus avoiding the need to either call `config.has()` first, or wrap `config.get()`
* inside a try-catch block every time.
* @param {string} key the configuration value to look up. Must be either true, false, or not exist in the config.
* @returns {boolean} `true` if key exists in config and is true, `false` otherwise
*/
getConfigBoolean(key) {
try {
const getConfig = config.get(key);

// isTruthy() can't handle undefined / null, so we have to do that here
// @see {@link https://github.com/node-config/node-config/wiki/Common-Usage#using-config-values}
if (getConfig === undefined || getConfig === null) return false;
else {
return module.exports.isTruthy(getConfig);
}
}
catch (e) {
return false;
}
},

/**
* @function getFileExtension
* From a string representing a filename, get the extension if there is one
Expand Down Expand Up @@ -77,6 +103,20 @@ module.exports = {
}
},

/**
* @function isTruthy
* Returns true if the element name in the object contains a truthy value
* @param {object} value The object to evaluate
* @returns {boolean} True if truthy, false if not, and undefined if undefined
*/
isTruthy(value) {
if (value === undefined) return value;

const isStr = typeof value === 'string' || value instanceof String;
const trueStrings = ['true', 't', 'yes', 'y', '1'];
return value === true || value === 1 || isStr && trueStrings.includes(value.toLowerCase());
},

/**
* @function prettyStringify
* Returns a pretty JSON representation of an object
Expand Down
3 changes: 2 additions & 1 deletion app/src/docs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const config = require('config');
const fs = require('fs');
const path = require('path');
const { load } = require('js-yaml');
const { getConfigBoolean } = require('../components/utils');

module.exports = {
/**
Expand Down Expand Up @@ -44,7 +45,7 @@ module.exports = {
const spec = load(rawSpec);
spec.servers[0].url = `/api/${version}`;

if (config.has('keycloak.enabled')) {
if (getConfigBoolean('keycloak.enabled')) {
// Dynamically update OIDC endpoint url
spec.components.securitySchemes.OpenID.openIdConnectUrl = `${config.get('keycloak.serverUrl')}/realms/${config.get('keycloak.realm')}/.well-known/openid-configuration`;
} else {
Expand Down
27 changes: 27 additions & 0 deletions app/src/docs/v2.api-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/FileTypes"
"401":
$ref: "#/components/responses/UnauthorizedError"
default:
description: Unexpected error
content:
Expand All @@ -51,6 +53,8 @@ paths:
responses:
"200":
description: Indicates API is running
"401":
$ref: "#/components/responses/UnauthorizedError"
default:
description: Unexpected error
content:
Expand Down Expand Up @@ -92,6 +96,8 @@ paths:
example: 742d642a4704eb1babd8122ce0f03f209354279ae8292bb3961d13e21578b855
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/UnauthorizedError"
"405":
description: Template already cached
content:
Expand Down Expand Up @@ -165,6 +171,8 @@ paths:
description: Raw binary-encoded response
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/UnauthorizedError"
"404":
$ref: "#/components/responses/NotFound"
default:
Expand Down Expand Up @@ -265,6 +273,8 @@ paths:
example: 742d642a4704eb1babd8122ce0f03f209354279ae8292bb3961d13e21578b855
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/UnauthorizedError"
"405":
$ref: "#/components/responses/MethodNotAllowed"
"422":
Expand Down Expand Up @@ -333,6 +343,8 @@ paths:
example: 742d642a4704eb1babd8122ce0f03f209354279ae8292bb3961d13e21578b855
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/UnauthorizedError"
"405":
$ref: "#/components/responses/MethodNotAllowed"
"422":
Expand Down Expand Up @@ -512,6 +524,17 @@ components:
- $ref: "#/components/schemas/InlineTemplateObject"
- type: object
description: An object containing the document template to merge into
UnauthorizedError:
allOf:
- $ref: "#/components/schemas/Problem"
- type: object
properties:
status:
example: 401
title:
example: Unauthorized
type:
example: "https://httpstatuses.com/401"
ValidationError:
allOf:
- $ref: "#/components/schemas/Problem"
Expand Down Expand Up @@ -571,6 +594,10 @@ components:
$ref: "#/components/schemas/NotFound"
UnauthorizedError:
description: Access token is missing or invalid
content:
application/json:
schema:
$ref: "#/components/schemas/UnauthorizedError"
UnprocessableEntity:
description: >-
The server was unable to process the contained instructions. Generally
Expand Down
48 changes: 39 additions & 9 deletions app/src/middleware/authorization.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,49 @@
const config = require('config');
const jwt = require('jsonwebtoken');
const Problem = require('api-problem');

const keycloak = require('../components/keycloak');
const { getConfigBoolean } = require('../components/utils');

/**
* @function _spkiWrapper
* Wraps an SPKI key with PEM header and footer
* @param {string} spki The PEM-encoded Simple public-key infrastructure string
* @returns {string} The PEM-encoded SPKI with PEM header and footer
*/
const _spkiWrapper = (spki) => `-----BEGIN PUBLIC KEY-----\n${spki}\n-----END PUBLIC KEY-----`;

module.exports = {
/**
* @function protect
* Enables keycloak protect only if environment has it enabled
* @param {string} [role=undefined] Keycloak protect role-based authorization
* @returns {function} An express/connect compatible middleware function
* Enables JWT verification only if environment has it enabled.
*/
protect: (role = undefined) => {
if (config.has('keycloak.enabled')) {
return keycloak.protect(role);
authenticate: (req, res, next) => {
const authorization = req.get('Authorization');

if (getConfigBoolean('keycloak.enabled')) {
const bearerToken = authorization.substring(7);

if (config.has('keycloak.publicKey')) {
const publicKey = config.get('keycloak.publicKey');
const pemKey = publicKey.startsWith('-----BEGIN') ? publicKey : _spkiWrapper(publicKey);

try {
jwt.verify(bearerToken, pemKey, {
issuer: `${config.get('keycloak.serverUrl')}/realms/${config.get('keycloak.realm')}`
});

next();
} catch (err) {
return new Problem(401, {
detail: err.message
}).send(res);
}

} else {
throw new Error('OIDC environment variable KC_PUBLICKEY or keycloak.publicKey must be defined');
}

} else {
return (_req, _res, next) => next();
next();
}
}
};
8 changes: 4 additions & 4 deletions app/src/routes/v2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const fileTypesRouter = require('./fileTypes');
const healthRouter = require('./health');
const templateRouter = require('./template');

const { protect } = require('../../middleware/authorization');
const { authenticate } = require('../../middleware/authorization');
const { getDocs, getJsonSpec, getYamlSpec } = require('../../middleware/openapi');

const version = 'v2';
Expand Down Expand Up @@ -46,12 +46,12 @@ router.get('/api-spec.yaml', docsHelmet, getYamlSpec(version));
router.get('/docs', docsHelmet, getDocs(version));

/** File Types Router */
router.get('/fileTypes', protect(), fileTypesRouter);
router.get('/fileTypes', authenticate, fileTypesRouter);

/** Health Router */
router.use('/health', protect(), healthRouter);
router.use('/health', authenticate, healthRouter);

/** Template Router */
router.use('/template', protect(), templateRouter);
router.use('/template', authenticate, templateRouter);

module.exports = router;

0 comments on commit 2472018

Please sign in to comment.