const Enum = require('./enum');
const Errors = require('./errors');
const { MysteryBox } = require('@squeep/mystery-box');
+const { TOTP } = require('@squeep/totp');
const { name: packageName } = require('../package');
const _fileScope = common.fileScope(__filename);
class Authenticator {
+ /**
+ * @typedef {Object} AuthInfo
+ * @property {String} identifier
+ * @property {String} credentials
+ * @property {String=} otpKey
+ */
/**
* @param {Console} logger
* @param {*} db
this.mysteryBox.on('statistics', common.mysteryBoxLogger(logger, _fileScope(this.constructor.name)));
this.cookieLifespan = options.authenticator.inactiveSessionLifespanSeconds || 60 * 60 * 24 * 32;
+
+ this.TOTP = TOTP; // For ease of test stubbing.
}
/**
* Check local auth entries.
* Sets ctx.authenticatedId if valid.
+ * Sets ctx.otpNeeded if account has otpKey.
* @param {String} identifier
* @param {String} credential
* @param {Object} ctx
+ * @returns {Promise<Boolean>}
*/
async isValidIdentifierCredential(identifier, credential, ctx) {
const _scope = _fileScope('isValidIdentifierCredential');
const authData = await this.db.authenticationGet(dbCtx, identifier);
if (!authData) {
this.logger.debug(_scope, 'failed, invalid identifier', { ctx, identifier });
+ } else if (authData.credential.startsWith('$argon2')
+ && this.authnEnabled.includes('argon2')) {
+ isValid = await this.authn.argon2.verify(authData.credential, credential);
+ } else if (authData.credential.startsWith('$PAM$')
+ && this.authnEnabled.includes('pam')) {
+ isValid = this._isValidPAMIdentifier(identifier, credential);
} else {
- if (authData.credential.startsWith('$argon2')
- && this.authnEnabled.includes('argon2')) {
- isValid = await this.authn.argon2.verify(authData.credential, credential);
- } else if (authData.credential.startsWith('$PAM$')
- && this.authnEnabled.includes('pam')) {
- isValid = this._isValidPAMIdentifier(identifier, credential);
- } else {
- this.logger.error(_scope, 'failed, unknown or unsupported type of stored credential', { identifier, ctx });
- }
+ this.logger.error(_scope, 'failed, unknown or unsupported type of stored credential', { identifier, ctx });
}
if (this.authnEnabled.includes('DEBUG_ANY')) {
if (isValid) {
ctx.authenticationId = identifier;
+ ctx.otpNeeded = !!authData?.otpKey;
await this.db.authenticationSuccess(dbCtx, identifier);
}
}); // dbCtx
* Check system PAM.
* @param {String} identifier
* @param {String} credential
- * @returns {Boolean}
+ * @returns {Promise<Boolean>}
*/
async _isValidPAMIdentifier(identifier, credential) {
const _scope = _fileScope('_isValidPAMIdentifier');
}
+ /**
+ *
+ * @param {Object} state
+ * @param {String} state.key
+ * @param {Number} state.attempt
+ * @param {Number} state.epochMs
+ * @param {String} otp
+ * @returns {String} Enum.OTPResult
+ */
+ checkOTP(state, otp) {
+ const totp = new this.TOTP({
+ ...this.options?.authenticator?.otpOptions,
+ ...state,
+ });
+ const isValid = totp.validate(otp);
+ if (isValid) {
+ return Enum.OTPResult.Valid;
+ }
+ const otpAttemptsRemain = state.attempt <= (this.options?.authenticator?.otpAttemptsAllowed ?? 3);
+ const otpWithinWindow = state.epochMs + (this.options?.authenticator?.otpWindowMs ?? 600000) > Date.now();
+ if (otpAttemptsRemain && otpWithinWindow) {
+ return Enum.OTPResult.InvalidSoftFail;
+ }
+ return Enum.OTPResult.InvalidHardFail;
+ }
+
+
/**
* Check for valid Basic auth, updates ctx with identifier if valid.
* @param {String} credentials
* @param {Object} ctx
- * @returns {Boolean}
+ * @returns {Promise<Boolean>}
*/
async isValidBasic(credentials, ctx) {
const _scope = _fileScope('isValidBasic');
* Determine which Authorization header is available, and if it is valid.
* @param {String} authorizationHeader
* @param {Object} ctx
+ * @returns {Promise<Boolean>}
*/
async isValidAuthorization(authorizationHeader, ctx) {
const _scope = _fileScope('isValidAuthorization');
this.logger.debug(_scope, 'valid authorization', { ctx, sessionAlsoValid });
return true;
}
- } else {
- if (sessionAlsoValid
- && await this.sessionCheck(req, res, ctx, undefined, false, false)) {
- this.logger.debug(_scope, 'valid session', { ctx, sessionAlsoValid });
- return true;
- }
+ } else if (sessionAlsoValid
+ && await this.sessionCheck(req, res, ctx, undefined, false, false)) {
+ this.logger.debug(_scope, 'valid session', { ctx, sessionAlsoValid });
+ return true;
}
this.logger.debug(_scope, 'invalid authorization', { ctx, sessionAlsoValid });