UNPKG

happn-3

Version:

pub/sub api as a service using primus and mongo & redis or nedb, can work as cluster, single process or embedded using nedb

247 lines (208 loc) 7.81 kB
const SecurityAuthProviderFactory = require('../factories/security-auth-provider-factory'); const commons = require('happn-commons'); module.exports = class SecurityBaseAuthProvider { #securityFacade; #config; #options; #locks; constructor(securityFacade, happnConfig, providerOptions) { this.#securityFacade = securityFacade; this.#config = happnConfig || {}; this.#options = this.defaults(commons._.clone(providerOptions || {})); if (!this.#config.accountLockout) { this.#config.accountLockout = {}; } if (this.#config.accountLockout.enabled == null) { this.#config.accountLockout.enabled = true; } if (this.#config.accountLockout.enabled === true) { this.#locks = this.#securityFacade.cache.getOrCreate('security_account_lockout'); if (!this.#config.accountLockout.attempts) { this.#config.accountLockout.attempts = 4; } if (!this.#config.accountLockout.retryInterval) { this.#config.accountLockout.retryInterval = 60 * 1000 * 10; //10 minutes } } } get securityFacade() { return this.#securityFacade; } get config() { return this.#config; } get options() { return this.#options; } get locks() { return this.#locks; } static create(providerConfig, securityFacade, happnConfig) { const providerPathOrInstance = providerConfig.provider != null ? providerConfig.provider : providerConfig; // we attach an instance directly to the config if (providerPathOrInstance instanceof SecurityBaseAuthProvider) { return providerPathOrInstance; } const options = providerConfig.options || {}; // we attach a factory directly to the config if (providerPathOrInstance instanceof SecurityAuthProviderFactory) { return providerPathOrInstance.create(securityFacade, happnConfig, options); } // we are using a filepath let providerClass; try { providerClass = require(providerPathOrInstance); } catch (e) { securityFacade.log.error( `failed to resolve or instantiate auth provider on path: ${providerPathOrInstance}` ); throw e; } const providerInst = new providerClass(securityFacade, happnConfig, options); // we are using a filepath that points to a factory if (providerInst instanceof SecurityAuthProviderFactory) { return providerInst.create(securityFacade, happnConfig, options); } return providerInst; } accessDenied(errorMessage) { throw this.#securityFacade.error.AccessDeniedError(errorMessage); } invalidCredentials(errorMessage) { throw this.#securityFacade.error.InvalidCredentialsError(errorMessage); } systemError(errorMessage) { throw this.#securityFacade.error.SystemError(errorMessage); } async login(credentials, sessionId, request) { if (credentials.username === '_ANONYMOUS') { if (!this.#config.allowAnonymousAccess) { throw this.#securityFacade.error.InvalidCredentialsError('Anonymous access is disabled'); } credentials.password = 'anonymous'; } //default is a stateful login if (credentials.type == null) credentials.type = 1; if ( !((credentials.username && (credentials.password || credentials.digest)) || credentials.token) ) { return this.invalidCredentials('Invalid credentials'); } if ( !this.#securityFacade.security.checkIPAddressWhitelistPolicy(credentials, sessionId, request) ) { return this.invalidCredentials('Source address access restricted'); } if ( this.#securityFacade.security.checkDisableDefaultAdminNetworkConnections(credentials, request) ) { return this.accessDenied('use of _ADMIN credentials over the network is disabled'); } if (credentials.token) { return this.tokenLogin(credentials, sessionId, request); } if (this.#checkLockedOut(credentials.username)) { return this.accessDenied('Account locked out'); } return this.providerCredsLogin(credentials, sessionId); } async tokenLogin(credentials, sessionId, request) { let [authorized, reason] = this.#securityFacade.utils.coerceArray( await this.#securityFacade.security.checkRevocations(credentials) ); if (!authorized) return this.accessDenied(reason); let previousSession = this.#securityFacade.security.decodeToken(credentials.token); if (previousSession == null) { return this.invalidCredentials('Invalid credentials: invalid session token'); } if (previousSession.ttl > 0) { if (Date.now() - previousSession.timestamp > previousSession.ttl) { return this.invalidCredentials('Invalid credentials: token timed out'); } } if (previousSession && previousSession.type != null && this.#config.lockTokenToLoginType) { if (previousSession.type !== credentials.type) { return this.accessDenied( `token was created using the login type ${previousSession.type}, which does not match how the new token is to be created` ); } } if ( this.#securityFacade.security.checkDisableDefaultAdminNetworkConnections( previousSession, request ) ) { return this.accessDenied('use of _ADMIN credentials over the network is disabled'); } let previousPolicy = previousSession.policy[1]; //always the stateful policy if (previousPolicy.disallowTokenLogins) { return this.accessDenied(`logins with this token are disallowed by policy`); } if ( previousPolicy.lockTokenToOrigin && previousSession.origin !== this.#securityFacade.system.name ) { return this.accessDenied(`this token is locked to a different origin by policy`); } return this.providerTokenLogin(credentials, previousSession, sessionId); } async providerTokenLogin() { return this.systemError('providerTokenLogin not implemented.'); } async providerCredsLogin() { return this.systemError('providerCredsLogin not implemented.'); } async providerResetPassword() { return this.systemError('providerResetPassword not implemented.'); } async providerChangePassword() { return this.systemError('providerChangePassword not implemented.'); } loginFailed(username, specificMessage, e, overrideLockout) { let message = 'Invalid credentials'; if (specificMessage) message = specificMessage; if (e) { if (e.message) message = message + ': ' + e.message; else message = message + ': ' + e.toString(); } if (this.#config.accountLockout && this.#config.accountLockout.enabled && !overrideLockout) { let currentLock = this.#locks.get(username); if (!currentLock) { currentLock = { attempts: 0, }; } currentLock.attempts++; this.#locks.set(username, currentLock, { ttl: this.#config.accountLockout.retryInterval, }); } return this.invalidCredentials(message); } loginOK(credentials, user, sessionId, tokenLogin, additionalInfo) { delete user.password; if (this.#locks) this.#locks.remove(user.username); //remove previous locks const session = this.#securityFacade.security.generateSession( user, sessionId, credentials, tokenLogin, additionalInfo ); if (session == null) { throw new Error('session disconnected during login'); } return session; } #checkLockedOut(username) { if (!username || !this.#config.accountLockout || !this.#config.accountLockout.enabled) return false; let existingLock = this.#locks.get(username); return existingLock != null && existingLock.attempts >= this.#config.accountLockout.attempts; } defaults(options) { return options; } };