@enteocode/nestjs-mfa
Version:
Implementation agnostic RFC-compliant Multi-Factor Authentication (2FA/MFA) module for NestJS with recovery code support
3 lines (2 loc) • 10.9 kB
JavaScript
import{ConfigurableModuleBuilder as e,Injectable as t,Inject as r,Logger as s,UnsupportedMediaTypeException as i,StreamableFile as o,PreconditionFailedException as n,UnauthorizedException as a,Global as c,InternalServerErrorException as l,Module as u,SetMetadata as h,createParamDecorator as d}from"@nestjs/common";import{DiscoveryService as g,ModuleRef as E,Reflector as p,DiscoveryModule as f}from"@nestjs/core";import{EventEmitter2 as m,OnEvent as v,EventEmitterModule as y}from"@nestjs/event-emitter";import{randomBytes as b,scryptSync as R,createCipheriv as A,createDecipheriv as w}from"node:crypto";import{v5 as S}from"uuid";import{serialize as F,deserialize as C}from"node:v8";import{toFileStream as D}from"qrcode";import O from"sharp";import{Authenticator as M}from"@otplib/core";import{createDigest as I,createRandomBytes as T}from"@otplib/plugin-crypto";import{keyDecoder as x,keyEncoder as N}from"@otplib/plugin-thirty-two";import{registerDecorator as _}from"class-validator";function Y(e,t,r,s){var i,o=arguments.length,n=o<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--)(i=e[a])&&(n=(o<3?i(n):o>3?i(t,r,n):i(t,r))||n);return o>3&&n&&Object.defineProperty(t,r,n),n}function L(e,t){return function(r,s){t(r,s,e)}}function V(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)}"function"==typeof SuppressedError&&SuppressedError;const B=e=>`/nestjs-mfa:${e.name}`,P=(e,t)=>`enteocode:mfa:${t}:${S(String(e),"00000000-0000-0000-0000-000000000000")}`,{ConfigurableModuleClass:j,MODULE_OPTIONS_TOKEN:k,OPTIONS_TYPE:z,ASYNC_OPTIONS_TYPE:U}=(new e).setClassMethodName("forRoot").setExtras({isGlobal:!0}).build();var G;let q=G=class CipherService{constructor({cipher:e}){this.logger=new s(B(G)),this.secret=e}encrypt(e){if(!this.secret)return e;const t=b(16),r=b(12),s=R(this.secret,t,32),i=A("aes-256-gcm",s,r),o=Buffer.concat([i.update(e),i.final()]),n=i.getAuthTag();return Buffer.concat([r,t,n,o])}decrypt(e){if(!this.secret)return e;const t=e.subarray(0,12),r=e.subarray(12,28),s=e.subarray(28,44),i=R(this.secret,r,32),o=w("aes-256-gcm",i,t);o.setAuthTag(s);try{return Buffer.concat([o.update(e.subarray(44)),o.final()])}catch(e){this.logger.error("Couldn't decrypt secret",{message:e.message})}}};var K;q=G=Y([t(),L(0,r(k)),V("design:paramtypes",[Object])],q),function(e){e.SECRET="secret",e.RECOVERY_CODES="recovery-codes"}(K||(K={}));let $=class SerializerService{serialize(e){return F(e)}deserialize(e){return C(e)}};var J;$=Y([t()],$);let Q=J=class StorageService{constructor(e,t,r){this.cipher=t,this.serializer=r,this.logger=new s(B(J)),this.store=e.store,this.serializer=e.serializer||r}has(e,t){return this.store.has(P(e,t))}async set(e,t,r){const s=P(e,r),i=this.serializer.serialize(t),o=this.cipher.encrypt(i)||i,n=await this.store.set(s,o);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(P(e,t));return r?this.serializer.deserialize(this.cipher.decrypt(r)||r):t===K.RECOVERY_CODES?null:""}delete(e,t){return this.store.delete(P(e,t))}};var W;Q=J=Y([t(),L(0,r(k)),V("design:paramtypes",[Object,q,$])],Q),function(e){e.AVIF="image/avif",e.PNG="image/png",e.JPG="image/jpeg",e.GIF="image/gif",e.WEBP="image/webp"}(W||(W={}));class InvalidFormatException extends i{}let H=class QrCodeService{async generate(e,t){const r=O().removeAlpha().grayscale(!0);if(await D(r,e),t===W.AVIF)return this.getStreamableFile(r.avif(),t);if(t===W.PNG)return this.getStreamableFile(r.png({colors:2}),t);if(t===W.JPG)return this.getStreamableFile(r.jpeg({mozjpeg:!0,progressive:!0}),t);if(t===W.WEBP)return this.getStreamableFile(r.webp({lossless:!0}),t);if(t===W.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(e,t){return new o(e,{type:t})}};H=Y([t()],H);let X=class OtpService{constructor(){this.authenticator=new M({keyEncoder:N,keyDecoder:x,createRandomBytes:T,createDigest:I})}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)}};X=Y([t()],X);class AuthenticationNotEnabledException extends n{}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 Z,ee,te;!function(e){e.TIMEOUT="timeout",e.AUTHENTICATOR="authenticator"}(Z||(Z={})),function(e){e.ENABLED="mfa.enabled",e.DISABLED="mfa.disabled",e.FAILED="mfa.failed",e.RECOVERY_ENABLED="mfa.recovery.enabled",e.RECOVERY_DISABLED="mfa.recovery.disabled",e.RECOVERY_USED="mfa.recovery.used",e.RECOVERY_FAILED="mfa.recovery.failed"}(ee||(ee={}));class AuthenticationFailedException extends a{}let re=te=class MfaService{constructor(e,t,r,i,o){this.options=e,this.otp=t,this.storage=r,this.qr=i,this.emitter=o,this.logger=new s(B(te))}async isEnabled(e){return await this.storage.has(e,K.SECRET)}async enable(e){const t=this.otp.generateSecret();return await this.storage.set(e,t,K.SECRET)?(this.logger.log("MFA is enabled for user",{user:e}),this.emitter.emit(ee.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,K.SECRET);return t&&(this.logger.log("MFA is disabled for user",{user:e}),this.emitter.emit(ee.DISABLED,new AuthenticationDisabledEvent(e))),t}async verify(e,t){const r=await this.storage.get(e,K.SECRET);if(!r)throw this.emitter.emit(ee.FAILED,new AuthenticationFailedEvent(e)),new AuthenticationNotEnabledException({user:e},{cause:"MFA is not enabled"});if(!this.otp.verify(r,t))throw this.emitter.emit(ee.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,K.SECRET);if(!s)throw new AuthenticationNotEnabledException({user:e},{cause:"MFA is not enabled"});const i="object"==typeof r;if(t===Z.TIMEOUT){const e={step:this.options.ttl||30},t=i?r:{};return this.otp.generateToken(s,{...e,...t})}const o=this.otp.generateKeyUri(s,this.options.issuer,String(e));return!r||i?o:await this.qr.generate(o,r)}};re=te=Y([t(),L(0,r(k)),V("design:paramtypes",[Object,X,Q,H,m])],re);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}}var se;let ie=se=class MfaRecoveryService{constructor(e,t,r){this.storage=e,this.otp=t,this.emitter=r,this.logger=new s(B(se))}async enable(e,t=10,r=10){if(!await this.storage.has(e,K.SECRET))return null;const s=new Set;for(;s.size<t;)s.add(this.otp.generateSecret(r));return await this.storage.set(e,s,K.RECOVERY_CODES)?(this.logger.log("MFA recovery enabled for user",{user:e,count:t}),this.emitter.emit(ee.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,K.RECOVERY_CODES);return t&&(this.logger.log("MFA recovery disabled",{user:e}),this.emitter.emit(ee.RECOVERY_DISABLED,new RecoveryDisabledEvent(e))),t}async recover(e,t){const r=await this.storage.get(e,K.RECOVERY_CODES);if(!r)return this.logger.error("MFA recovery failed",{user:e,reason:"No recovery codes persisted"}),this.emitter.emit(ee.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(ee.RECOVERY_FAILED,new RecoveryFailedEvent(e,t)),!1;return!!await this.storage.set(e,r,K.RECOVERY_CODES)&&(this.logger.log("MFA recovery success",{user:e,code:t}),this.emitter.emit(ee.RECOVERY_USED,new RecoveryUsedEvent(e,t)),!0)}};ie=se=Y([t(),V("design:paramtypes",[Q,X,m])],ie);let oe=class MfaEventListener{constructor(e){this.recovery=e}async onMfaDisabled(e){await this.recovery.disable(e.user)}};Y([v(ee.DISABLED),V("design:type",Function),V("design:paramtypes",[AuthenticationDisabledEvent]),V("design:returntype",Promise)],oe.prototype,"onMfaDisabled",null),oe=Y([t(),V("design:paramtypes",[ie])],oe);const ne=Symbol();var ae;let ce=ae=class MfaCredentialsExtractorManager{constructor(e,t,r){this.discovery=e,this.ref=t,this.reflector=r,this.logger=new s(B(ae)),this.memory=new Map}onModuleInit(){const e=this.discovery.getProviders();for(const{metatype:t}of e){if(!t)continue;const e=this.reflector.get(ne,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}};ce=ae=Y([t(),V("design:paramtypes",[g,E,p])],ce);let le=class MfaCredentialsPipe{constructor(e,t){this.manager=e,this.service=t}async transform({options:e,context:t}){const{required:r,validate:s}=this.getOptions(e),i=this.manager.resolve(t);if(!i&&r)throw new l("No credentials extractor for the given context",{cause:t.getType()});if(!i)return null;const o=i.getUserIdentifier(t),n=i.getToken(t);return s&&await this.service.verify(o,n),{user:o,token:n}}getOptions(e){const t={required:!1,validate:!1};return e?{...t,...e}:t}};le=Y([t(),c(),V("design:paramtypes",[ce,re])],le);let ue=class MfaModule extends j{static forRoot(e){return super.forRoot(e)}static forRootAsync(e){return super.forRootAsync(e)}};ue=Y([u({imports:[y,f],exports:[re,ie],providers:[$,q,Q,H,X,re,ie,oe,ce,le]})],ue);const he=e=>h(ne,e||"all"),de=e=>d(((t,r)=>({context:r,options:e})))(le);function ge(e){return function(t,r){_({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}/))}})}}export{AuthenticationDisabledEvent,AuthenticationEnabledEvent,AuthenticationFailedEvent,AuthenticationFailedException,AuthenticationNotEnabledException,ee as EventType,W as Format,InvalidFormatException,ge as IsToken,de as MfaCredentials,he as MfaCredentialsExtractor,ue as MfaModule,ie as MfaRecoveryService,re as MfaService,RecoveryDisabledEvent,RecoveryEnabledEvent,RecoveryFailedEvent,Z as TokenType};
//# sourceMappingURL=index.mjs.map