import crypto from 'crypto'; import { AppConfigDomainIAM, AppConfigDomainIAMAuthenticationStep, ApplicationError, ConfigProviderService, LoggerService } from '@node-c/core'; import ld from 'lodash'; import { IAMAuthenticationUserLocalCompleteData, IAMAuthenticationUserLocalCompleteOptions, IAMAuthenticationUserLocalCompleteResult, IAMAuthenticationUserLocalGetUserAuthenticationConfigResult, IAMAuthenticationUserLocalInitiateData, IAMAuthenticationUserLocalInitiateOptions, IAMAuthenticationUserLocalInitiateResult } from './iam.authenticationUserLocal.definitions'; import { IAMAuthenticationService } from '../authentication'; import { IAMMFAService, IAMMFAType } from '../mfa'; /** * A service for authentication using a local user and password. * * This service is intended for use by the consumer environment. */ export class IAMAuthenticationUserLocalService< CompleteContext extends object, InitiateContext extends object > extends IAMAuthenticationService { constructor( configProvider: ConfigProviderService, logger: LoggerService, moduleName: string, serviceName: string, // eslint-disable-next-line no-unused-vars protected mfaServices?: Record> ) { super(configProvider, logger, moduleName, serviceName); this.isLocal = true; } async complete( data: IAMAuthenticationUserLocalCompleteData, options: IAMAuthenticationUserLocalCompleteOptions ): Promise { const { configProvider, logger, moduleName, mfaServices, serviceName } = this; const { defaultUserIdentifierField } = configProvider.config.domain[moduleName] as AppConfigDomainIAM; const { mfaData, mfaType } = data; const { context, mfaOptions } = options; const userIdentifierField = options.contextIdentifierField || defaultUserIdentifierField; const userIdentifierValue = context[userIdentifierField as keyof CompleteContext]; let mfaUsed = false; let mfaValid = false; if (mfaType) { const mfaService = mfaServices?.[mfaType]; if (!mfaService) { logger.error( `[${moduleName}][${serviceName}]: Login attempt failed for user "${userIdentifierValue}" - MFA service ${mfaType} not configured.` ); throw new ApplicationError('Authentication failed.'); } if (!mfaData) { logger.error( `[${moduleName}][${serviceName}]: Login attempt failed for user "${userIdentifierValue}" - no MFA data provided.` ); throw new ApplicationError('Authentication failed.'); } const mfaResult = await mfaService.complete(mfaData, { ...(mfaOptions || {}), context }); mfaUsed = true; mfaValid = mfaResult.valid; } return { mfaUsed, mfaValid, valid: true }; } getUserAuthenticationConfig(): IAMAuthenticationUserLocalGetUserAuthenticationConfigResult { const { configProvider, moduleName, serviceName } = this; const moduleConfig = configProvider.config.domain[moduleName] as AppConfigDomainIAM; const { steps } = moduleConfig.authServiceSettings![serviceName]; const defaultConfig: IAMAuthenticationUserLocalGetUserAuthenticationConfigResult = { [AppConfigDomainIAMAuthenticationStep.Complete]: { cache: { settings: { // we call the user's id "state" here, since "state" is also used as the cache key for the oauth2 flow cacheFieldName: 'state', inputFieldName: 'options.context.id' }, use: { options: { overwrite: true, use: true } } }, findUser: true, findUserBeforeAuth: true, validWithoutUser: false }, [AppConfigDomainIAMAuthenticationStep.Initiate]: { cache: { populate: { options: [{ cacheFieldName: 'context', inputFieldName: 'options.context' }] }, settings: { // we call the user's id "state" here, since "state" is also used as the cache key for the oauth2 flow cacheFieldName: 'state', inputFieldName: 'options.context.id' } }, findUser: true, findUserBeforeAuth: true, validWithoutUser: false } }; return ld.merge(defaultConfig, steps || {}); } async initiate( data: IAMAuthenticationUserLocalInitiateData, options: IAMAuthenticationUserLocalInitiateOptions ): Promise { const { configProvider, logger, moduleName, mfaServices, serviceName } = this; const moduleConfig = configProvider.config.domain[moduleName] as AppConfigDomainIAM; const { secretKeyHMACAlgorithm, hashingSecret } = moduleConfig.authServiceSettings![serviceName].secretKey!; const { mfaData, mfaType, password: authPassword } = data; const { context, context: { password: userPassword }, mfaOptions } = options; const userIdentifierField = options.contextIdentifierField || moduleConfig.defaultUserIdentifierField; const userIdentifierValue = context[userIdentifierField as keyof InitiateContext]; let mfaUsed = false; let mfaValid = false; let wrongPassword = false; if (!secretKeyHMACAlgorithm || !hashingSecret || !userPassword) { wrongPassword = true; logger.error( `[${moduleName}][${serviceName}]: secretKeyHMACAlgorithm, hashingSecret and/or userPassword not provided.` ); } else { const computedPassword = crypto .createHmac(secretKeyHMACAlgorithm, hashingSecret) .update(`${authPassword}`) .digest('hex') .toString(); if (computedPassword !== userPassword) { wrongPassword = true; } } if (wrongPassword) { logger.error( `[${moduleName}][${serviceName}]: Login attempt failed for user "${userIdentifierValue}" - wrong password.` ); throw new ApplicationError('Authentication failed.'); } if (mfaType) { const mfaService = mfaServices?.[mfaType]; if (!mfaService) { logger.error( `[${moduleName}][${serviceName}]: Login attempt failed for user "${userIdentifierValue}" - MFA service ${mfaType} not configured.` ); throw new ApplicationError('Authentication failed.'); } if (!mfaData) { logger.error( `[${moduleName}][${serviceName}]: Login attempt failed for user "${userIdentifierValue}" - no MFA data provided.` ); throw new ApplicationError('Authentication failed.'); } const mfaResult = await mfaService.initiate(mfaData, { ...(mfaOptions || {}), context }); mfaUsed = true; mfaValid = mfaResult.valid; } return { mfaUsed, mfaValid, valid: true }; } }