Created
June 21, 2018 10:59
-
-
Save ManuelRauber/077607ce0d28be636a7f7e0470bf9d1c to your computer and use it in GitHub Desktop.
Restify Identity Server Reference Token Validation Middleware
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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