UNPKG

@enteocode/nestjs-mfa

Version:

Implementation agnostic RFC-compliant Multi-Factor Authentication (2FA/MFA) module for NestJS with recovery code support

3 lines (2 loc) 11.7 kB
"use strict";var e=require("@nestjs/common"),t=require("@nestjs/core"),r=require("@nestjs/event-emitter"),s=require("node:crypto"),o=require("uuid"),i=require("node:v8"),n=require("qrcode"),a=require("sharp"),c=require("@otplib/core"),l=require("@otplib/plugin-crypto"),u=require("@otplib/plugin-thirty-two"),p=require("class-validator");function d(e,t,r,s){var o,i=arguments.length,n=i<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,r):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(e,t,r,s);else for(var a=e.length-1;a>=0;a--)(o=e[a])&&(n=(i<3?o(n):i>3?o(t,r,n):o(t,r))||n);return i>3&&n&&Object.defineProperty(t,r,n),n}function h(e,t){return function(r,s){t(r,s,e)}}function E(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)}"function"==typeof SuppressedError&&SuppressedError;const v=e=>`@enteocode/nestjs-mfa:${e.name}`,g=(e,t)=>`enteocode:mfa:${t}:${o.v5(String(e),"00000000-0000-0000-0000-000000000000")}`,{ConfigurableModuleClass:y,MODULE_OPTIONS_TOKEN:f,OPTIONS_TYPE:m,ASYNC_OPTIONS_TYPE:b}=(new e.ConfigurableModuleBuilder).setClassMethodName("forRoot").setExtras({isGlobal:!0}).build();var x;let R=x=class CipherService{constructor({cipher:t}){this.logger=new e.Logger(v(x)),this.secret=t}encrypt(e){if(!this.secret)return e;const t=s.randomBytes(16),r=s.randomBytes(12),o=s.scryptSync(this.secret,t,32),i=s.createCipheriv("aes-256-gcm",o,r),n=Buffer.concat([i.update(e),i.final()]),a=i.getAuthTag();return Buffer.concat([r,t,a,n])}decrypt(e){if(!this.secret)return e;const t=e.subarray(0,12),r=e.subarray(12,28),o=e.subarray(28,44),i=s.scryptSync(this.secret,r,32),n=s.createDecipheriv("aes-256-gcm",i,t);n.setAuthTag(o);try{return Buffer.concat([n.update(e.subarray(44)),n.final()])}catch(e){this.logger.error("Couldn't decrypt secret",{message:e.message})}}};var S;R=x=d([e.Injectable(),h(0,e.Inject(f)),E("design:paramtypes",[Object])],R),function(e){e.SECRET="secret",e.RECOVERY_CODES="recovery-codes"}(S||(S={}));let A=class SerializerService{serialize(e){return i.serialize(e)}deserialize(e){return i.deserialize(e)}};var F;A=d([e.Injectable()],A);let M=F=class StorageService{constructor(t,r,s){this.cipher=r,this.serializer=s,this.logger=new e.Logger(v(F)),this.store=t.store,this.serializer=t.serializer||s}has(e,t){return this.store.has(g(e,t))}async set(e,t,r){const s=g(e,r),o=this.serializer.serialize(t),i=this.cipher.encrypt(o)||o,n=await this.store.set(s,i);return n?this.logger.debug("Secret saved",{id:s,user:e}):this.logger.error("Cannot save secret",{id:s,user:e}),n}async get(e,t){const r=await this.store.get(g(e,t));return r?this.serializer.deserialize(this.cipher.decrypt(r)||r):t===S.RECOVERY_CODES?null:""}delete(e,t){return this.store.delete(g(e,t))}};var w;M=F=d([e.Injectable(),h(0,e.Inject(f)),E("design:paramtypes",[Object,R,A])],M),exports.Format=void 0,(w=exports.Format||(exports.Format={})).AVIF="image/avif",w.PNG="image/png",w.JPG="image/jpeg",w.GIF="image/gif",w.WEBP="image/webp";class InvalidFormatException extends e.UnsupportedMediaTypeException{}let D=class QrCodeService{async generate(e,t){const r=a().removeAlpha().grayscale(!0);if(await n.toFileStream(r,e),t===exports.Format.AVIF)return this.getStreamableFile(r.avif(),t);if(t===exports.Format.PNG)return this.getStreamableFile(r.png({colors:2}),t);if(t===exports.Format.JPG)return this.getStreamableFile(r.jpeg({mozjpeg:!0,progressive:!0}),t);if(t===exports.Format.WEBP)return this.getStreamableFile(r.webp({lossless:!0}),t);if(t===exports.Format.GIF)return this.getStreamableFile(r.gif({colors:2,reuse:!0}),t);throw new InvalidFormatException("Format to generate QR Code is not supported",{cause:t})}getStreamableFile(t,r){return new e.StreamableFile(t,{type:r})}};D=d([e.Injectable()],D);let T=class OtpService{constructor(){this.authenticator=new c.Authenticator({keyEncoder:u.keyEncoder,keyDecoder:u.keyDecoder,createRandomBytes:l.createRandomBytes,createDigest:l.createDigest})}generateSecret(e=20){return this.authenticator.generateSecret(e)}generateToken(e,t){const r=Date.now(),s=t?{epoch:r,...t}:{epoch:r};return this.authenticator.clone(s).generate(e)}generateKeyUri(e,t,r){return this.authenticator.keyuri(r,t,e)}verify(e,t){return this.authenticator.check(t,e)}};T=d([e.Injectable()],T);class AuthenticationNotEnabledException extends e.PreconditionFailedException{}class AuthenticationEnabledEvent{constructor(e,t){this.user=e,this.secret=t}}class AuthenticationDisabledEvent{constructor(e){this.user=e}}class AuthenticationFailedEvent{constructor(e,t){this.user=e,this.token=t}}var C,I,O,j;exports.TokenType=void 0,(C=exports.TokenType||(exports.TokenType={})).TIMEOUT="timeout",C.AUTHENTICATOR="authenticator",exports.EventType=void 0,(I=exports.EventType||(exports.EventType={})).ENABLED="mfa.enabled",I.DISABLED="mfa.disabled",I.FAILED="mfa.failed",I.RECOVERY_ENABLED="mfa.recovery.enabled",I.RECOVERY_DISABLED="mfa.recovery.disabled",I.RECOVERY_USED="mfa.recovery.used",I.RECOVERY_FAILED="mfa.recovery.failed";class AuthenticationFailedException extends e.UnauthorizedException{}exports.MfaService=O=class MfaService{constructor(t,r,s,o,i){this.options=t,this.otp=r,this.storage=s,this.qr=o,this.emitter=i,this.logger=new e.Logger(v(O))}async isEnabled(e){return await this.storage.has(e,S.SECRET)}async enable(e){const t=this.otp.generateSecret();return await this.storage.set(e,t,S.SECRET)?(this.logger.log("MFA is enabled for user",{user:e}),this.emitter.emit(exports.EventType.ENABLED,new AuthenticationEnabledEvent(e,t)),t):(this.logger.error("Cannot enable MFA for user",{user:e}),"")}async disable(e){const t=await this.storage.delete(e,S.SECRET);return t&&(this.logger.log("MFA is disabled for user",{user:e}),this.emitter.emit(exports.EventType.DISABLED,new AuthenticationDisabledEvent(e))),t}async verify(e,t){const r=await this.storage.get(e,S.SECRET);if(!r)throw this.emitter.emit(exports.EventType.FAILED,new AuthenticationFailedEvent(e)),new AuthenticationNotEnabledException({user:e},{cause:"MFA is not enabled"});if(!this.otp.verify(r,t))throw this.emitter.emit(exports.EventType.FAILED,new AuthenticationFailedEvent(e,t)),new AuthenticationFailedException({user:e},{cause:"MFA token invalid"})}async generate(e,t,r){const s=await this.storage.get(e,S.SECRET);if(!s)throw new AuthenticationNotEnabledException({user:e},{cause:"MFA is not enabled"});const o="object"==typeof r;if(t===exports.TokenType.TIMEOUT){const e={step:this.options.ttl||30},t=o?r:{};return this.otp.generateToken(s,{...e,...t})}const i=this.otp.generateKeyUri(s,this.options.issuer,String(e));return!r||o?i:await this.qr.generate(i,r)}},exports.MfaService=O=d([e.Injectable(),h(0,e.Inject(f)),E("design:paramtypes",[Object,T,M,D,r.EventEmitter2])],exports.MfaService);class RecoveryEnabledEvent{constructor(e,t){this.user=e,this.codes=t}}class RecoveryFailedEvent{constructor(e,t){this.user=e,this.code=t}}class RecoveryUsedEvent{constructor(e,t){this.user=e,this.code=t}}class RecoveryDisabledEvent{constructor(e){this.user=e}}exports.MfaRecoveryService=j=class MfaRecoveryService{constructor(t,r,s){this.storage=t,this.otp=r,this.emitter=s,this.logger=new e.Logger(v(j))}async enable(e,t=10,r=10){if(!await this.storage.has(e,S.SECRET))return null;const s=new Set;for(;s.size<t;)s.add(this.otp.generateSecret(r));return await this.storage.set(e,s,S.RECOVERY_CODES)?(this.logger.log("MFA recovery enabled for user",{user:e,count:t}),this.emitter.emit(exports.EventType.RECOVERY_ENABLED,new RecoveryEnabledEvent(e,s)),s):(this.logger.error("Cannot enable MFA recovery for user",{user:e}),null)}async disable(e){const t=await this.storage.delete(e,S.RECOVERY_CODES);return t&&(this.logger.log("MFA recovery disabled",{user:e}),this.emitter.emit(exports.EventType.RECOVERY_DISABLED,new RecoveryDisabledEvent(e))),t}async recover(e,t){const r=await this.storage.get(e,S.RECOVERY_CODES);if(!r)return this.logger.error("MFA recovery failed",{user:e,reason:"No recovery codes persisted"}),this.emitter.emit(exports.EventType.RECOVERY_FAILED,new RecoveryFailedEvent(e,t)),!1;if(!r.delete(t))return this.logger.error("MFA recovery failed",{user:e,reason:"Invalid code"}),this.emitter.emit(exports.EventType.RECOVERY_FAILED,new RecoveryFailedEvent(e,t)),!1;return!!await this.storage.set(e,r,S.RECOVERY_CODES)&&(this.logger.log("MFA recovery success",{user:e,code:t}),this.emitter.emit(exports.EventType.RECOVERY_USED,new RecoveryUsedEvent(e,t)),!0)}},exports.MfaRecoveryService=j=d([e.Injectable(),E("design:paramtypes",[M,T,r.EventEmitter2])],exports.MfaRecoveryService);let L=class MfaEventListener{constructor(e){this.recovery=e}async onMfaDisabled(e){await this.recovery.disable(e.user)}};d([r.OnEvent(exports.EventType.DISABLED),E("design:type",Function),E("design:paramtypes",[AuthenticationDisabledEvent]),E("design:returntype",Promise)],L.prototype,"onMfaDisabled",null),L=d([e.Injectable(),E("design:paramtypes",[exports.MfaRecoveryService])],L);const N=Symbol();var B;let _=B=class MfaCredentialsExtractorManager{constructor(t,r,s){this.discovery=t,this.ref=r,this.reflector=s,this.logger=new e.Logger(v(B)),this.memory=new Map}onModuleInit(){const e=this.discovery.getProviders();for(const{metatype:t}of e){if(!t)continue;const e=this.reflector.get(N,t);if(!e)continue;const r=this.ref.get(t,{strict:!1});r&&(this.logger.debug("MFA context resolver discovered",{meta:t.name,type:e}),this.add(e,r))}}add(e,t){const{memory:r}=this;r.has(e)||r.set(e,new Set),r.get(e).add(t)}*get(e){const{memory:t}=this;t.has(e)&&(yield*t.get(e)),"all"!==e&&t.has("all")&&(yield*t.get("all"))}resolve(e){const t=e.getType();for(const r of this.get(t))if(r.supports(e))return r;return null}};_=B=d([e.Injectable(),E("design:paramtypes",[t.DiscoveryService,t.ModuleRef,t.Reflector])],_);let k=class MfaCredentialsPipe{constructor(e,t){this.manager=e,this.service=t}async transform({options:t,context:r}){const{required:s,validate:o}=this.getOptions(t),i=this.manager.resolve(r);if(!i&&s)throw new e.InternalServerErrorException("No credentials extractor for the given context",{cause:r.getType()});if(!i)return null;const n=i.getUserIdentifier(r),a=i.getToken(r);return o&&await this.service.verify(n,a),{user:n,token:a}}getOptions(e){const t={required:!1,validate:!1};return e?{...t,...e}:t}};k=d([e.Injectable(),e.Global(),E("design:paramtypes",[_,exports.MfaService])],k),exports.MfaModule=class MfaModule extends y{static forRoot(e){return super.forRoot(e)}static forRootAsync(e){return super.forRootAsync(e)}},exports.MfaModule=d([e.Module({imports:[r.EventEmitterModule,t.DiscoveryModule],exports:[exports.MfaService,exports.MfaRecoveryService],providers:[A,R,M,D,T,exports.MfaService,exports.MfaRecoveryService,L,_,k]})],exports.MfaModule);exports.AuthenticationDisabledEvent=AuthenticationDisabledEvent,exports.AuthenticationEnabledEvent=AuthenticationEnabledEvent,exports.AuthenticationFailedEvent=AuthenticationFailedEvent,exports.AuthenticationFailedException=AuthenticationFailedException,exports.AuthenticationNotEnabledException=AuthenticationNotEnabledException,exports.InvalidFormatException=InvalidFormatException,exports.IsToken=function(e){return function(t,r){p.registerDecorator({target:t.constructor,name:"IsToken",options:e,propertyName:r,validator:{defaultMessage:()=>"Invalid MFA token",validate:e=>Boolean(e&&"string"==typeof e&&e.match(/[0-9]{6}/))}})}},exports.MfaCredentials=t=>e.createParamDecorator(((e,r)=>({context:r,options:t})))(k),exports.MfaCredentialsExtractor=t=>e.SetMetadata(N,t||"all"),exports.RecoveryDisabledEvent=RecoveryDisabledEvent,exports.RecoveryEnabledEvent=RecoveryEnabledEvent,exports.RecoveryFailedEvent=RecoveryFailedEvent; //# sourceMappingURL=index.js.map