Skip to content

Instantly share code, notes, and snippets.

@ManuelRauber
Created June 21, 2018 10:59
Show Gist options
  • Save ManuelRauber/077607ce0d28be636a7f7e0470bf9d1c to your computer and use it in GitHub Desktop.
Save ManuelRauber/077607ce0d28be636a7f7e0470bf9d1c to your computer and use it in GitHub Desktop.
Restify Identity Server Reference Token Validation Middleware
import { boolean } from 'joi';
import * as restify from 'restify';
import { RequestHandler, Server } from 'restify';
import * as corsMiddleware from 'restify-cors-middleware';
import { CONTROLLERS } from '../controllers';
import { ReferenceTokenValidationConfiguration } from '../models/referenceTokenValidationConfiguration';
import { DOCUMENT_DB_SERVICES } from '../services';
import { loggerService } from '../services/logger';
import { AuthenticatedRequestHandler, HttpServer } from './httpServer';
import { ReferenceTokenValidation } from './referenceTokenValidation';
export class ApiServer implements HttpServer {
private restify: Server;
private _referenceTokenValidation: ReferenceTokenValidation;
get(
url: string,
isAnonymous?: boolean | AuthenticatedRequestHandler | RequestHandler,
...requestHandlers: (AuthenticatedRequestHandler | RequestHandler)[]
) {
this.addRoute('get', url, isAnonymous, ...requestHandlers);
}
post(
url: string,
isAnonymous?: boolean | AuthenticatedRequestHandler | RequestHandler,
...requestHandlers: (AuthenticatedRequestHandler | RequestHandler)[]
) {
this.addRoute('post', url, isAnonymous, ...requestHandlers);
}
del(
url: string,
isAnonymous?: boolean | AuthenticatedRequestHandler | RequestHandler,
...requestHandlers: (AuthenticatedRequestHandler | RequestHandler)[]
) {
this.addRoute('del', url, isAnonymous, ...requestHandlers);
}
put(
url: string,
isAnonymous?: boolean | AuthenticatedRequestHandler | RequestHandler,
...requestHandlers: (AuthenticatedRequestHandler | RequestHandler)[]
) {
this.addRoute('put', url, isAnonymous, ...requestHandlers);
}
async start(port: number) {
this.restify = restify.createServer({
version: '1.0.0',
name: 'BusinessFriends API',
});
this._referenceTokenValidation = await this.createRefrenceTokenValidation();
this.addRestifyPlugins();
CONTROLLERS.forEach(controller => controller.initialize(this));
await Promise.all(DOCUMENT_DB_SERVICES.map(service => service.initialize()));
this.restify.listen(port, () => loggerService.info(`Server is up and running on port ${port}`));
}
private addRestifyPlugins() {
const cors = corsMiddleware({
origins: ['*'],
allowHeaders: ['Authorization'],
} as any);
this.restify.pre(cors.preflight);
this.restify.use(cors.actual);
this.restify.use(restify.plugins.queryParser());
this.restify.use(restify.plugins.bodyParser());
}
private addRoute(
method: 'get' | 'post' | 'put' | 'del',
url: string,
isAnonymous?: boolean | AuthenticatedRequestHandler | RequestHandler,
...requestHandlers: (AuthenticatedRequestHandler | RequestHandler)[]
) {
let anonymous: boolean = true;
if (typeof isAnonymous !== 'boolean') {
requestHandlers.unshift(isAnonymous);
requestHandlers.unshift(this._referenceTokenValidation.validate());
anonymous = false;
}
if (typeof isAnonymous === 'boolean') {
if (!isAnonymous) {
requestHandlers.unshift(this._referenceTokenValidation.validate());
anonymous = false;
}
}
const lastHandler = requestHandlers.pop();
const apiHandler: any = async (req, res, next) => {
try {
await lastHandler(req, res, next);
}
catch (e) {
loggerService.fatal('Error during routing', e);
res.send(500);
}
};
requestHandlers.push(apiHandler);
this.restify[method](url, ...requestHandlers);
loggerService.debug(`Added ${anonymous ? 'anonymous' : 'secured'} route ${method.toUpperCase()} /${url}`);
}
private async createRefrenceTokenValidation(): Promise<ReferenceTokenValidation> {
const referenceTokenValidation = new ReferenceTokenValidation(new ReferenceTokenValidationConfiguration(
process.env.AUTHORITY_URL,
process.env.SCOPE_NAME,
process.env.SCOPE_SECRET,
));
await referenceTokenValidation.initialize();
return referenceTokenValidation;
}
}
import { Request, RequestHandler, Response } from 'restify';
import { AuthenticatedRequest } from '../models/authenticatedRequest';
import { ReferenceTokenValidationConfiguration } from '../models/referenceTokenValidationConfiguration';
import { loggerService } from '../services/logger';
import request = require('request');
import errors = require('restify-errors');
class OpenIdConfiguration {
constructor(public introspectionEndpoint: string) {
}
static loadConfiguration(authorityUrl: string): Promise<OpenIdConfiguration> {
return new Promise((resolve, reject) => {
request.get(`${authorityUrl}.well-known/openid-configuration`, (err, response, body) => {
if (err || !body) {
return reject('Could not get openid configuration.');
}
const configuration = JSON.parse(body);
const openIdConfiguration = new OpenIdConfiguration(configuration.introspection_endpoint);
loggerService.info('Got OpenID Configuration', openIdConfiguration);
resolve(openIdConfiguration);
});
});
}
}
export class ReferenceTokenValidation {
private _openIdConfiguration: OpenIdConfiguration;
constructor(private readonly _options: ReferenceTokenValidationConfiguration) {
}
async initialize(): Promise<void> {
this._openIdConfiguration = await OpenIdConfiguration.loadConfiguration(this._options.authorityUrl);
}
validate(): RequestHandler {
return (req: Request, res: Response, next: Function) => {
loggerService.info('checking token for request', req.url);
const token = this._getTokenFromHeader(req);
if (!token) {
return next(new errors.UnauthorizedError());
}
const authHeader = new Buffer(`${this._options.scopeName}:${this._options.scopeSecret}`).toString('base64');
request.post(this._openIdConfiguration.introspectionEndpoint, {
form: {
token: token,
},
headers: {
Authorization: `Basic ${authHeader}`,
},
}, (err, response, body) => {
loggerService.info('Got result from authority', body, 'for request', req.url);
if (!body) {
loggerService.info('Authority did not send a token result');
return next(new errors.UnauthorizedError());
}
const tokenResult = JSON.parse(body);
const isTokenValid = this._isTokenValid(tokenResult);
// If it returns an error or the statusCode is not 200 OK, return a 401 Not authorized
if (err || response.statusCode !== 200 || !isTokenValid) {
loggerService.info('Token seems not to be valid for request', req.url, err);
return next(new errors.UnauthorizedError());
}
(<AuthenticatedRequest>req).user = tokenResult;
// If everything is ok, go to the next middleware
return next();
});
};
}
private _getTokenFromHeader(req: Request): string {
const authorizationHeader = req.header('authorization');
// No header given
if (!authorizationHeader) {
loggerService.info('Authorization header not found for request', req.url, req.rawHeaders);
return '';
}
// Not a bearer token
if (!authorizationHeader.toLowerCase().startsWith('bearer')) {
loggerService.info('Token is not a bearer token for request', req.url);
return '';
}
const token = authorizationHeader.substr(7);
// Seems to be a JWT and we don't support JWT :)
if (token.indexOf('.') > -1) {
loggerService.info('Token seems to be a JWT, which is not supported for request', req.url);
return '';
}
return token;
}
private _isTokenValid(tokenResult: any): boolean {
if (!tokenResult) {
loggerService.info('Token invalid: No token found');
return false;
}
if (typeof(tokenResult.active) !== 'undefined' && !tokenResult.active) {
loggerService.info('Token invalid: It is not active');
return false;
}
if (!tokenResult.scope || !tokenResult.scope.length) {
loggerService.info('Token invalid: Scope not set');
return false;
}
if (tokenResult.scope.indexOf(this._options.scopeName) === -1) {
loggerService.info(`Token invalid: Got scope ${tokenResult.scope}, but ${this._options.scopeName} is needed`);
return false;
}
return true;
}
}
export class ReferenceTokenValidationConfiguration {
authorityUrl: string;
scopeName: string;
scopeSecret: string;
constructor(authorityUrl: string, scopeName: string, scopeSecret: string) {
if (!authorityUrl) {
throw new Error('authorityUrl is not set');
}
if (!scopeName) {
throw new Error('scopeName is not set');
}
if (!scopeSecret) {
throw new Error('scopeSecret is not set');
}
this.authorityUrl = authorityUrl;
this.scopeName = scopeName;
this.scopeSecret = scopeSecret;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment