X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=src%2Fauthenticator.js;fp=src%2Fauthenticator.js;h=ed4fc401af7771302b523732981d4f45abfe9ead;hb=9696c012e6b9a6c58904baa397ca0ebf78112316;hp=0000000000000000000000000000000000000000;hpb=f59e918f3aba3a218c94a252072801fc40527647;p=websub-hub diff --git a/src/authenticator.js b/src/authenticator.js new file mode 100644 index 0000000..ed4fc40 --- /dev/null +++ b/src/authenticator.js @@ -0,0 +1,113 @@ +'use strict'; + +const argon2 = require('argon2'); +const common = require('./common'); +const Enum = require('./enum'); +const Errors = require('./errors'); + +const _fileScope = common.fileScope(__filename); + +class Authenticator { + constructor(logger, db, options) { + this.logger = logger; + this.db = db; + this.basicRealm = options.authenticator.basicRealm; + this.secureAuthOnly = options.authenticator.secureAuthOnly; + } + + + /** + * Check for valid Basic auth, updates ctx with identifier if valid. + * @param {String} credentials + * @param {Object} ctx + * @returns {Boolean} + */ + async isValidBasic(credentials, ctx) { + const _scope = _fileScope('isValidBasic'); + this.logger.debug(_scope, 'called', { ctx }); + + const [identifier, credential] = common.splitFirst(credentials, ':', ''); + + let valid = false; + await this.db.context(async (dbCtx) => { + const authData = await this.db.authenticationGet(dbCtx, identifier); + if (!authData) { + this.logger.debug(_scope, 'failed, invalid authentication id', { ctx }); + return false; + } + + if (authData.credential.startsWith('$argon2')) { + valid = await argon2.verify(authData.credential, credential); + } else { + this.logger.error(_scope, 'failed, unknown type of stored password hash', { ctx }); + } + if (valid) { + ctx.authenticationId = identifier; + await this.db.authenticationSuccess(dbCtx, identifier); + } + }); + + return valid; + } + + + /** + * Determine which Authorization header is available, and if it is valid. + * @param {String} authorizationHeader + * @param {Object} ctx + */ + async isValidAuthorization(authorizationHeader, ctx) { + const _scope = _fileScope('isValidAuthorization'); + this.logger.debug(_scope, 'called', { authorizationHeader, ctx }); + + const [authMethod, authString] = common.splitFirst(authorizationHeader, ' ', '').map((x) => x.trim()); + // eslint-disable-next-line sonarjs/no-small-switch + switch (authMethod.toLowerCase()) { + case 'basic': { + const credentials = Buffer.from(authString, 'base64').toString('utf-8'); + return await this.isValidBasic(credentials, ctx); + } + + default: + this.logger.debug(_scope, 'unknown authorization scheme', { ctx }); + return false; + } + } + + + /** + * Send a response requesting basic auth. + * @param {http.ServerResponse} res + */ + requestBasic(res) { + res.setHeader(Enum.Header.WWWAuthenticate, `Basic realm="${this.basicRealm}", charset="UTF-8"`); + throw new Errors.ResponseError(Enum.ErrorResponse.Unauthorized); + } + + + /** + * Require that a request has valid auth over secure channel, requests if missing. + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {Object} ctx + */ + async required(req, res, ctx) { + const _scope = _fileScope('required'); + this.logger.debug(_scope, 'called', { ctx }); + + if (this.secureAuthOnly && ctx.clientProtocol.toLowerCase() !== 'https') { + this.logger.debug(_scope, 'rejecting insecure auth', ctx); + throw new Errors.ResponseError(Enum.ErrorResponse.Forbidden, 'authentication required, but connection is insecure; cannot continue'); + } + + const authData = req.getHeader(Enum.Header.Authorization); + if (authData + && await this.isValidAuthorization(authData, ctx)) { + return true; + } + return this.requestBasic(res); + } + +} + +module.exports = Authenticator; \ No newline at end of file