@nauth-toolkit/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/database-columns.d.ts +10 -0
- package/dist/adapters/database-columns.d.ts.map +1 -0
- package/dist/adapters/database-columns.js +85 -0
- package/dist/adapters/database-columns.js.map +1 -0
- package/dist/adapters/express.adapter.d.ts +41 -0
- package/dist/adapters/express.adapter.d.ts.map +1 -0
- package/dist/adapters/express.adapter.js +188 -0
- package/dist/adapters/express.adapter.js.map +1 -0
- package/dist/adapters/fastify.adapter.d.ts +33 -0
- package/dist/adapters/fastify.adapter.d.ts.map +1 -0
- package/dist/adapters/fastify.adapter.js +223 -0
- package/dist/adapters/fastify.adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +25 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/storage.factory.d.ts +7 -0
- package/dist/adapters/storage.factory.d.ts.map +1 -0
- package/dist/adapters/storage.factory.js +24 -0
- package/dist/adapters/storage.factory.js.map +1 -0
- package/dist/bootstrap.d.ts +41 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +113 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/dto/auth-challenge.dto.d.ts +19 -0
- package/dist/dto/auth-challenge.dto.d.ts.map +1 -0
- package/dist/dto/auth-challenge.dto.js +86 -0
- package/dist/dto/auth-challenge.dto.js.map +1 -0
- package/dist/dto/auth-response.dto.d.ts +31 -0
- package/dist/dto/auth-response.dto.d.ts.map +1 -0
- package/dist/dto/auth-response.dto.js +18 -0
- package/dist/dto/auth-response.dto.js.map +1 -0
- package/dist/dto/challenge-response.dto.d.ts +36 -0
- package/dist/dto/challenge-response.dto.d.ts.map +1 -0
- package/dist/dto/challenge-response.dto.js +3 -0
- package/dist/dto/challenge-response.dto.js.map +1 -0
- package/dist/dto/change-password-request.dto.d.ts +5 -0
- package/dist/dto/change-password-request.dto.d.ts.map +1 -0
- package/dist/dto/change-password-request.dto.js +30 -0
- package/dist/dto/change-password-request.dto.js.map +1 -0
- package/dist/dto/change-password-response.dto.d.ts +4 -0
- package/dist/dto/change-password-response.dto.d.ts.map +1 -0
- package/dist/dto/change-password-response.dto.js +8 -0
- package/dist/dto/change-password-response.dto.js.map +1 -0
- package/dist/dto/change-password.dto.d.ts +5 -0
- package/dist/dto/change-password.dto.d.ts.map +1 -0
- package/dist/dto/change-password.dto.js +29 -0
- package/dist/dto/change-password.dto.js.map +1 -0
- package/dist/dto/error-response.dto.d.ts +9 -0
- package/dist/dto/error-response.dto.d.ts.map +1 -0
- package/dist/dto/error-response.dto.js +59 -0
- package/dist/dto/error-response.dto.js.map +1 -0
- package/dist/dto/get-available-methods.dto.d.ts +7 -0
- package/dist/dto/get-available-methods.dto.d.ts.map +1 -0
- package/dist/dto/get-available-methods.dto.js +33 -0
- package/dist/dto/get-available-methods.dto.js.map +1 -0
- package/dist/dto/get-challenge-data-response.dto.d.ts +4 -0
- package/dist/dto/get-challenge-data-response.dto.d.ts.map +1 -0
- package/dist/dto/get-challenge-data-response.dto.js +8 -0
- package/dist/dto/get-challenge-data-response.dto.js.map +1 -0
- package/dist/dto/get-challenge-data.dto.d.ts +8 -0
- package/dist/dto/get-challenge-data.dto.d.ts.map +1 -0
- package/dist/dto/get-challenge-data.dto.js +40 -0
- package/dist/dto/get-challenge-data.dto.js.map +1 -0
- package/dist/dto/get-client-info.dto.d.ts +17 -0
- package/dist/dto/get-client-info.dto.d.ts.map +1 -0
- package/dist/dto/get-client-info.dto.js +20 -0
- package/dist/dto/get-client-info.dto.js.map +1 -0
- package/dist/dto/get-device-token-response.dto.d.ts +4 -0
- package/dist/dto/get-device-token-response.dto.d.ts.map +1 -0
- package/dist/dto/get-device-token-response.dto.js +8 -0
- package/dist/dto/get-device-token-response.dto.js.map +1 -0
- package/dist/dto/get-events-by-type.dto.d.ts +17 -0
- package/dist/dto/get-events-by-type.dto.d.ts.map +1 -0
- package/dist/dto/get-events-by-type.dto.js +20 -0
- package/dist/dto/get-events-by-type.dto.js.map +1 -0
- package/dist/dto/get-ip-address-response.dto.d.ts +4 -0
- package/dist/dto/get-ip-address-response.dto.d.ts.map +1 -0
- package/dist/dto/get-ip-address-response.dto.js +8 -0
- package/dist/dto/get-ip-address-response.dto.js.map +1 -0
- package/dist/dto/get-mfa-status.dto.d.ts +16 -0
- package/dist/dto/get-mfa-status.dto.d.ts.map +1 -0
- package/dist/dto/get-mfa-status.dto.js +41 -0
- package/dist/dto/get-mfa-status.dto.js.map +1 -0
- package/dist/dto/get-risk-assessment-history.dto.d.ts +9 -0
- package/dist/dto/get-risk-assessment-history.dto.d.ts.map +1 -0
- package/dist/dto/get-risk-assessment-history.dto.js +13 -0
- package/dist/dto/get-risk-assessment-history.dto.js.map +1 -0
- package/dist/dto/get-session-id-response.dto.d.ts +4 -0
- package/dist/dto/get-session-id-response.dto.d.ts.map +1 -0
- package/dist/dto/get-session-id-response.dto.js +8 -0
- package/dist/dto/get-session-id-response.dto.js.map +1 -0
- package/dist/dto/get-setup-data-response.dto.d.ts +4 -0
- package/dist/dto/get-setup-data-response.dto.d.ts.map +1 -0
- package/dist/dto/get-setup-data-response.dto.js +8 -0
- package/dist/dto/get-setup-data-response.dto.js.map +1 -0
- package/dist/dto/get-setup-data.dto.d.ts +7 -0
- package/dist/dto/get-setup-data.dto.d.ts.map +1 -0
- package/dist/dto/get-setup-data.dto.js +43 -0
- package/dist/dto/get-setup-data.dto.js.map +1 -0
- package/dist/dto/get-suspicious-activity.dto.d.ts +9 -0
- package/dist/dto/get-suspicious-activity.dto.d.ts.map +1 -0
- package/dist/dto/get-suspicious-activity.dto.js +13 -0
- package/dist/dto/get-suspicious-activity.dto.js.map +1 -0
- package/dist/dto/get-user-agent-response.dto.d.ts +4 -0
- package/dist/dto/get-user-agent-response.dto.d.ts.map +1 -0
- package/dist/dto/get-user-agent-response.dto.js +8 -0
- package/dist/dto/get-user-agent-response.dto.js.map +1 -0
- package/dist/dto/get-user-auth-history.dto.d.ts +20 -0
- package/dist/dto/get-user-auth-history.dto.d.ts.map +1 -0
- package/dist/dto/get-user-auth-history.dto.js +22 -0
- package/dist/dto/get-user-auth-history.dto.js.map +1 -0
- package/dist/dto/get-user-by-email.dto.d.ts +5 -0
- package/dist/dto/get-user-by-email.dto.d.ts.map +1 -0
- package/dist/dto/get-user-by-email.dto.js +36 -0
- package/dist/dto/get-user-by-email.dto.js.map +1 -0
- package/dist/dto/get-user-by-id.dto.d.ts +4 -0
- package/dist/dto/get-user-by-id.dto.d.ts.map +1 -0
- package/dist/dto/get-user-by-id.dto.js +29 -0
- package/dist/dto/get-user-by-id.dto.js.map +1 -0
- package/dist/dto/get-user-devices.dto.d.ts +8 -0
- package/dist/dto/get-user-devices.dto.d.ts.map +1 -0
- package/dist/dto/get-user-devices.dto.js +33 -0
- package/dist/dto/get-user-devices.dto.js.map +1 -0
- package/dist/dto/get-user-response.dto.d.ts +2 -0
- package/dist/dto/get-user-response.dto.d.ts.map +1 -0
- package/dist/dto/get-user-response.dto.js +6 -0
- package/dist/dto/get-user-response.dto.js.map +1 -0
- package/dist/dto/has-provider.dto.d.ts +7 -0
- package/dist/dto/has-provider.dto.d.ts.map +1 -0
- package/dist/dto/has-provider.dto.js +38 -0
- package/dist/dto/has-provider.dto.js.map +1 -0
- package/dist/dto/index.d.ts +51 -0
- package/dist/dto/index.d.ts.map +1 -0
- package/dist/dto/index.js +67 -0
- package/dist/dto/index.js.map +1 -0
- package/dist/dto/is-trusted-device-response.dto.d.ts +4 -0
- package/dist/dto/is-trusted-device-response.dto.d.ts.map +1 -0
- package/dist/dto/is-trusted-device-response.dto.js +8 -0
- package/dist/dto/is-trusted-device-response.dto.js.map +1 -0
- package/dist/dto/list-providers-response.dto.d.ts +4 -0
- package/dist/dto/list-providers-response.dto.d.ts.map +1 -0
- package/dist/dto/list-providers-response.dto.js +8 -0
- package/dist/dto/list-providers-response.dto.js.map +1 -0
- package/dist/dto/login.dto.d.ts +7 -0
- package/dist/dto/login.dto.d.ts.map +1 -0
- package/dist/dto/login.dto.js +68 -0
- package/dist/dto/login.dto.js.map +1 -0
- package/dist/dto/logout-all-response.dto.d.ts +4 -0
- package/dist/dto/logout-all-response.dto.d.ts.map +1 -0
- package/dist/dto/logout-all-response.dto.js +8 -0
- package/dist/dto/logout-all-response.dto.js.map +1 -0
- package/dist/dto/logout-all.dto.d.ts +5 -0
- package/dist/dto/logout-all.dto.d.ts.map +1 -0
- package/dist/dto/logout-all.dto.js +42 -0
- package/dist/dto/logout-all.dto.js.map +1 -0
- package/dist/dto/logout-response.dto.d.ts +4 -0
- package/dist/dto/logout-response.dto.d.ts.map +1 -0
- package/dist/dto/logout-response.dto.js +8 -0
- package/dist/dto/logout-response.dto.js.map +1 -0
- package/dist/dto/logout.dto.d.ts +5 -0
- package/dist/dto/logout.dto.d.ts.map +1 -0
- package/dist/dto/logout.dto.js +36 -0
- package/dist/dto/logout.dto.js.map +1 -0
- package/dist/dto/refresh-token.dto.d.ts +4 -0
- package/dist/dto/refresh-token.dto.d.ts.map +1 -0
- package/dist/dto/refresh-token.dto.js +24 -0
- package/dist/dto/refresh-token.dto.js.map +1 -0
- package/dist/dto/remove-devices.dto.d.ts +9 -0
- package/dist/dto/remove-devices.dto.d.ts.map +1 -0
- package/dist/dto/remove-devices.dto.js +50 -0
- package/dist/dto/remove-devices.dto.js.map +1 -0
- package/dist/dto/resend-code-response.dto.d.ts +4 -0
- package/dist/dto/resend-code-response.dto.d.ts.map +1 -0
- package/dist/dto/resend-code-response.dto.js +8 -0
- package/dist/dto/resend-code-response.dto.js.map +1 -0
- package/dist/dto/resend-code.dto.d.ts +4 -0
- package/dist/dto/resend-code.dto.d.ts.map +1 -0
- package/dist/dto/resend-code.dto.js +29 -0
- package/dist/dto/resend-code.dto.js.map +1 -0
- package/dist/dto/reset-password.dto.d.ts +8 -0
- package/dist/dto/reset-password.dto.d.ts.map +1 -0
- package/dist/dto/reset-password.dto.js +61 -0
- package/dist/dto/reset-password.dto.js.map +1 -0
- package/dist/dto/respond-challenge.dto.d.ts +33 -0
- package/dist/dto/respond-challenge.dto.d.ts.map +1 -0
- package/dist/dto/respond-challenge.dto.js +131 -0
- package/dist/dto/respond-challenge.dto.js.map +1 -0
- package/dist/dto/set-mfa-exemption.dto.d.ts +12 -0
- package/dist/dto/set-mfa-exemption.dto.d.ts.map +1 -0
- package/dist/dto/set-mfa-exemption.dto.js +66 -0
- package/dist/dto/set-mfa-exemption.dto.js.map +1 -0
- package/dist/dto/set-must-change-password-response.dto.d.ts +4 -0
- package/dist/dto/set-must-change-password-response.dto.d.ts.map +1 -0
- package/dist/dto/set-must-change-password-response.dto.js +8 -0
- package/dist/dto/set-must-change-password-response.dto.js.map +1 -0
- package/dist/dto/set-must-change-password.dto.d.ts +4 -0
- package/dist/dto/set-must-change-password.dto.d.ts.map +1 -0
- package/dist/dto/set-must-change-password.dto.js +29 -0
- package/dist/dto/set-must-change-password.dto.js.map +1 -0
- package/dist/dto/set-preferred-method.dto.d.ts +8 -0
- package/dist/dto/set-preferred-method.dto.d.ts.map +1 -0
- package/dist/dto/set-preferred-method.dto.js +49 -0
- package/dist/dto/set-preferred-method.dto.js.map +1 -0
- package/dist/dto/setup-mfa.dto.d.ts +9 -0
- package/dist/dto/setup-mfa.dto.d.ts.map +1 -0
- package/dist/dto/setup-mfa.dto.js +55 -0
- package/dist/dto/setup-mfa.dto.js.map +1 -0
- package/dist/dto/signup.dto.d.ts +10 -0
- package/dist/dto/signup.dto.d.ts.map +1 -0
- package/dist/dto/signup.dto.js +109 -0
- package/dist/dto/signup.dto.js.map +1 -0
- package/dist/dto/social-auth.dto.d.ts +54 -0
- package/dist/dto/social-auth.dto.d.ts.map +1 -0
- package/dist/dto/social-auth.dto.js +232 -0
- package/dist/dto/social-auth.dto.js.map +1 -0
- package/dist/dto/trust-device-response.dto.d.ts +4 -0
- package/dist/dto/trust-device-response.dto.d.ts.map +1 -0
- package/dist/dto/trust-device-response.dto.js +8 -0
- package/dist/dto/trust-device-response.dto.js.map +1 -0
- package/dist/dto/trust-device.dto.d.ts +1 -0
- package/dist/dto/trust-device.dto.d.ts.map +1 -0
- package/dist/dto/trust-device.dto.js +2 -0
- package/dist/dto/trust-device.dto.js.map +1 -0
- package/dist/dto/update-user-attributes-request.dto.d.ts +5 -0
- package/dist/dto/update-user-attributes-request.dto.d.ts.map +1 -0
- package/dist/dto/update-user-attributes-request.dto.js +30 -0
- package/dist/dto/update-user-attributes-request.dto.js.map +1 -0
- package/dist/dto/user-response.dto.d.ts +20 -0
- package/dist/dto/user-response.dto.d.ts.map +1 -0
- package/dist/dto/user-response.dto.js +42 -0
- package/dist/dto/user-response.dto.js.map +1 -0
- package/dist/dto/user-update.dto.d.ts +12 -0
- package/dist/dto/user-update.dto.d.ts.map +1 -0
- package/dist/dto/user-update.dto.js +119 -0
- package/dist/dto/user-update.dto.js.map +1 -0
- package/dist/dto/verify-email.dto.d.ts +29 -0
- package/dist/dto/verify-email.dto.d.ts.map +1 -0
- package/dist/dto/verify-email.dto.js +161 -0
- package/dist/dto/verify-email.dto.js.map +1 -0
- package/dist/dto/verify-mfa-code.dto.d.ts +10 -0
- package/dist/dto/verify-mfa-code.dto.d.ts.map +1 -0
- package/dist/dto/verify-mfa-code.dto.js +56 -0
- package/dist/dto/verify-mfa-code.dto.js.map +1 -0
- package/dist/dto/verify-phone-by-sub.dto.d.ts +6 -0
- package/dist/dto/verify-phone-by-sub.dto.d.ts.map +1 -0
- package/dist/dto/verify-phone-by-sub.dto.js +49 -0
- package/dist/dto/verify-phone-by-sub.dto.js.map +1 -0
- package/dist/dto/verify-phone.dto.d.ts +24 -0
- package/dist/dto/verify-phone.dto.d.ts.map +1 -0
- package/dist/dto/verify-phone.dto.js +124 -0
- package/dist/dto/verify-phone.dto.js.map +1 -0
- package/dist/entities/auth-audit.entity.d.ts +31 -0
- package/dist/entities/auth-audit.entity.d.ts.map +1 -0
- package/dist/entities/auth-audit.entity.js +33 -0
- package/dist/entities/auth-audit.entity.js.map +1 -0
- package/dist/entities/challenge-session.entity.d.ts +17 -0
- package/dist/entities/challenge-session.entity.d.ts.map +1 -0
- package/dist/entities/challenge-session.entity.js +21 -0
- package/dist/entities/challenge-session.entity.js.map +1 -0
- package/dist/entities/index.d.ts +12 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/index.js +26 -0
- package/dist/entities/index.js.map +1 -0
- package/dist/entities/login-attempt.entity.d.ts +13 -0
- package/dist/entities/login-attempt.entity.d.ts.map +1 -0
- package/dist/entities/login-attempt.entity.js +17 -0
- package/dist/entities/login-attempt.entity.js.map +1 -0
- package/dist/entities/mfa-device.entity.d.ts +22 -0
- package/dist/entities/mfa-device.entity.d.ts.map +1 -0
- package/dist/entities/mfa-device.entity.js +25 -0
- package/dist/entities/mfa-device.entity.js.map +1 -0
- package/dist/entities/rate-limit.entity.d.ts +9 -0
- package/dist/entities/rate-limit.entity.d.ts.map +1 -0
- package/dist/entities/rate-limit.entity.js +13 -0
- package/dist/entities/rate-limit.entity.js.map +1 -0
- package/dist/entities/session.entity.d.ts +32 -0
- package/dist/entities/session.entity.d.ts.map +1 -0
- package/dist/entities/session.entity.js +36 -0
- package/dist/entities/session.entity.js.map +1 -0
- package/dist/entities/social-account.entity.d.ts +13 -0
- package/dist/entities/social-account.entity.d.ts.map +1 -0
- package/dist/entities/social-account.entity.js +17 -0
- package/dist/entities/social-account.entity.js.map +1 -0
- package/dist/entities/storage-lock.entity.d.ts +8 -0
- package/dist/entities/storage-lock.entity.d.ts.map +1 -0
- package/dist/entities/storage-lock.entity.js +12 -0
- package/dist/entities/storage-lock.entity.js.map +1 -0
- package/dist/entities/trusted-device.entity.d.ts +17 -0
- package/dist/entities/trusted-device.entity.d.ts.map +1 -0
- package/dist/entities/trusted-device.entity.js +21 -0
- package/dist/entities/trusted-device.entity.js.map +1 -0
- package/dist/entities/user.entity.d.ts +41 -0
- package/dist/entities/user.entity.d.ts.map +1 -0
- package/dist/entities/user.entity.js +45 -0
- package/dist/entities/user.entity.js.map +1 -0
- package/dist/entities/verification-token.entity.d.ts +19 -0
- package/dist/entities/verification-token.entity.d.ts.map +1 -0
- package/dist/entities/verification-token.entity.js +29 -0
- package/dist/entities/verification-token.entity.js.map +1 -0
- package/dist/enums/auth-audit-event-type.enum.d.ts +55 -0
- package/dist/enums/auth-audit-event-type.enum.d.ts.map +1 -0
- package/dist/enums/auth-audit-event-type.enum.js +59 -0
- package/dist/enums/auth-audit-event-type.enum.js.map +1 -0
- package/dist/enums/error-codes.enum.d.ts +53 -0
- package/dist/enums/error-codes.enum.d.ts.map +1 -0
- package/dist/enums/error-codes.enum.js +57 -0
- package/dist/enums/error-codes.enum.js.map +1 -0
- package/dist/enums/mfa-method.enum.d.ts +11 -0
- package/dist/enums/mfa-method.enum.d.ts.map +1 -0
- package/dist/enums/mfa-method.enum.js +18 -0
- package/dist/enums/mfa-method.enum.js.map +1 -0
- package/dist/enums/risk-factor.enum.d.ts +14 -0
- package/dist/enums/risk-factor.enum.d.ts.map +1 -0
- package/dist/enums/risk-factor.enum.js +18 -0
- package/dist/enums/risk-factor.enum.js.map +1 -0
- package/dist/exceptions/nauth.exception.d.ts +18 -0
- package/dist/exceptions/nauth.exception.d.ts.map +1 -0
- package/dist/exceptions/nauth.exception.js +64 -0
- package/dist/exceptions/nauth.exception.js.map +1 -0
- package/dist/handlers/auth.handler.d.ts +18 -0
- package/dist/handlers/auth.handler.d.ts.map +1 -0
- package/dist/handlers/auth.handler.js +173 -0
- package/dist/handlers/auth.handler.js.map +1 -0
- package/dist/handlers/client-info.handler.d.ts +12 -0
- package/dist/handlers/client-info.handler.d.ts.map +1 -0
- package/dist/handlers/client-info.handler.js +61 -0
- package/dist/handlers/client-info.handler.js.map +1 -0
- package/dist/handlers/csrf.handler.d.ts +13 -0
- package/dist/handlers/csrf.handler.d.ts.map +1 -0
- package/dist/handlers/csrf.handler.js +84 -0
- package/dist/handlers/csrf.handler.js.map +1 -0
- package/dist/handlers/token-delivery.handler.d.ts +12 -0
- package/dist/handlers/token-delivery.handler.d.ts.map +1 -0
- package/dist/handlers/token-delivery.handler.js +86 -0
- package/dist/handlers/token-delivery.handler.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/client-info.interface.d.ts +16 -0
- package/dist/interfaces/client-info.interface.d.ts.map +1 -0
- package/dist/interfaces/client-info.interface.js +3 -0
- package/dist/interfaces/client-info.interface.js.map +1 -0
- package/dist/interfaces/config.interface.d.ts +279 -0
- package/dist/interfaces/config.interface.d.ts.map +1 -0
- package/dist/interfaces/config.interface.js +3 -0
- package/dist/interfaces/config.interface.js.map +1 -0
- package/dist/interfaces/entities.interface.d.ts +169 -0
- package/dist/interfaces/entities.interface.d.ts.map +1 -0
- package/dist/interfaces/entities.interface.js +3 -0
- package/dist/interfaces/entities.interface.js.map +1 -0
- package/dist/interfaces/index.d.ts +11 -0
- package/dist/interfaces/index.d.ts.map +1 -0
- package/dist/interfaces/index.js +27 -0
- package/dist/interfaces/index.js.map +1 -0
- package/dist/interfaces/logger.interface.d.ts +43 -0
- package/dist/interfaces/logger.interface.d.ts.map +1 -0
- package/dist/interfaces/logger.interface.js +12 -0
- package/dist/interfaces/logger.interface.js.map +1 -0
- package/dist/interfaces/mfa-provider.interface.d.ts +12 -0
- package/dist/interfaces/mfa-provider.interface.d.ts.map +1 -0
- package/dist/interfaces/mfa-provider.interface.js +3 -0
- package/dist/interfaces/mfa-provider.interface.js.map +1 -0
- package/dist/interfaces/oauth.interface.d.ts +24 -0
- package/dist/interfaces/oauth.interface.d.ts.map +1 -0
- package/dist/interfaces/oauth.interface.js +3 -0
- package/dist/interfaces/oauth.interface.js.map +1 -0
- package/dist/interfaces/provider.interface.d.ts +12 -0
- package/dist/interfaces/provider.interface.d.ts.map +1 -0
- package/dist/interfaces/provider.interface.js +3 -0
- package/dist/interfaces/provider.interface.js.map +1 -0
- package/dist/interfaces/social-auth-provider.interface.d.ts +13 -0
- package/dist/interfaces/social-auth-provider.interface.d.ts.map +1 -0
- package/dist/interfaces/social-auth-provider.interface.js +3 -0
- package/dist/interfaces/social-auth-provider.interface.js.map +1 -0
- package/dist/interfaces/storage-adapter.interface.d.ts +39 -0
- package/dist/interfaces/storage-adapter.interface.d.ts.map +1 -0
- package/dist/interfaces/storage-adapter.interface.js +3 -0
- package/dist/interfaces/storage-adapter.interface.js.map +1 -0
- package/dist/interfaces/template.interface.d.ts +99 -0
- package/dist/interfaces/template.interface.d.ts.map +1 -0
- package/dist/interfaces/template.interface.js +15 -0
- package/dist/interfaces/template.interface.js.map +1 -0
- package/dist/interfaces/token-verifier.interface.d.ts +7 -0
- package/dist/interfaces/token-verifier.interface.d.ts.map +1 -0
- package/dist/interfaces/token-verifier.interface.js +3 -0
- package/dist/interfaces/token-verifier.interface.js.map +1 -0
- package/dist/internal.d.ts +20 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +53 -0
- package/dist/internal.js.map +1 -0
- package/dist/platform/interfaces.d.ts +56 -0
- package/dist/platform/interfaces.d.ts.map +1 -0
- package/dist/platform/interfaces.js +3 -0
- package/dist/platform/interfaces.js.map +1 -0
- package/dist/schemas/auth-config.schema.d.ts +3411 -0
- package/dist/schemas/auth-config.schema.d.ts.map +1 -0
- package/dist/schemas/auth-config.schema.js +428 -0
- package/dist/schemas/auth-config.schema.js.map +1 -0
- package/dist/services/adaptive-mfa-decision.service.d.ts +39 -0
- package/dist/services/adaptive-mfa-decision.service.d.ts.map +1 -0
- package/dist/services/adaptive-mfa-decision.service.js +223 -0
- package/dist/services/adaptive-mfa-decision.service.js.map +1 -0
- package/dist/services/auth-audit.service.d.ts +44 -0
- package/dist/services/auth-audit.service.d.ts.map +1 -0
- package/dist/services/auth-audit.service.js +241 -0
- package/dist/services/auth-audit.service.js.map +1 -0
- package/dist/services/auth-challenge-helper.service.d.ts +48 -0
- package/dist/services/auth-challenge-helper.service.d.ts.map +1 -0
- package/dist/services/auth-challenge-helper.service.js +425 -0
- package/dist/services/auth-challenge-helper.service.js.map +1 -0
- package/dist/services/auth-flow-context-builder.service.d.ts +31 -0
- package/dist/services/auth-flow-context-builder.service.d.ts.map +1 -0
- package/dist/services/auth-flow-context-builder.service.js +253 -0
- package/dist/services/auth-flow-context-builder.service.js.map +1 -0
- package/dist/services/auth-flow-rules.d.ts +18 -0
- package/dist/services/auth-flow-rules.d.ts.map +1 -0
- package/dist/services/auth-flow-rules.js +55 -0
- package/dist/services/auth-flow-rules.js.map +1 -0
- package/dist/services/auth-flow-state-definitions.d.ts +5 -0
- package/dist/services/auth-flow-state-definitions.d.ts.map +1 -0
- package/dist/services/auth-flow-state-definitions.js +87 -0
- package/dist/services/auth-flow-state-definitions.js.map +1 -0
- package/dist/services/auth-flow-state-machine.service.d.ts +17 -0
- package/dist/services/auth-flow-state-machine.service.d.ts.map +1 -0
- package/dist/services/auth-flow-state-machine.service.js +91 -0
- package/dist/services/auth-flow-state-machine.service.js.map +1 -0
- package/dist/services/auth-flow-state-machine.types.d.ts +55 -0
- package/dist/services/auth-flow-state-machine.types.d.ts.map +1 -0
- package/dist/services/auth-flow-state-machine.types.js +16 -0
- package/dist/services/auth-flow-state-machine.types.js.map +1 -0
- package/dist/services/auth.service.d.ts +87 -0
- package/dist/services/auth.service.d.ts.map +1 -0
- package/dist/services/auth.service.js +2356 -0
- package/dist/services/auth.service.js.map +1 -0
- package/dist/services/challenge.service.d.ts +32 -0
- package/dist/services/challenge.service.d.ts.map +1 -0
- package/dist/services/challenge.service.js +293 -0
- package/dist/services/challenge.service.js.map +1 -0
- package/dist/services/client-info.service.d.ts +20 -0
- package/dist/services/client-info.service.d.ts.map +1 -0
- package/dist/services/client-info.service.js +202 -0
- package/dist/services/client-info.service.js.map +1 -0
- package/dist/services/csrf.service.d.ts +13 -0
- package/dist/services/csrf.service.d.ts.map +1 -0
- package/dist/services/csrf.service.js +67 -0
- package/dist/services/csrf.service.js.map +1 -0
- package/dist/services/email-verification.service.d.ts +30 -0
- package/dist/services/email-verification.service.d.ts.map +1 -0
- package/dist/services/email-verification.service.js +373 -0
- package/dist/services/email-verification.service.js.map +1 -0
- package/dist/services/geo-location.service.d.ts +85 -0
- package/dist/services/geo-location.service.d.ts.map +1 -0
- package/dist/services/geo-location.service.js +338 -0
- package/dist/services/geo-location.service.js.map +1 -0
- package/dist/services/index.d.ts +14 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +30 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/jwt.service.d.ts +62 -0
- package/dist/services/jwt.service.d.ts.map +1 -0
- package/dist/services/jwt.service.js +261 -0
- package/dist/services/jwt.service.js.map +1 -0
- package/dist/services/mfa-base.service.d.ts +37 -0
- package/dist/services/mfa-base.service.d.ts.map +1 -0
- package/dist/services/mfa-base.service.js +297 -0
- package/dist/services/mfa-base.service.js.map +1 -0
- package/dist/services/mfa.service.d.ts +35 -0
- package/dist/services/mfa.service.d.ts.map +1 -0
- package/dist/services/mfa.service.js +449 -0
- package/dist/services/mfa.service.js.map +1 -0
- package/dist/services/password.service.d.ts +19 -0
- package/dist/services/password.service.d.ts.map +1 -0
- package/dist/services/password.service.js +150 -0
- package/dist/services/password.service.js.map +1 -0
- package/dist/services/phone-verification.service.d.ts +32 -0
- package/dist/services/phone-verification.service.d.ts.map +1 -0
- package/dist/services/phone-verification.service.js +474 -0
- package/dist/services/phone-verification.service.js.map +1 -0
- package/dist/services/risk-detection.service.d.ts +30 -0
- package/dist/services/risk-detection.service.d.ts.map +1 -0
- package/dist/services/risk-detection.service.js +518 -0
- package/dist/services/risk-detection.service.js.map +1 -0
- package/dist/services/risk-scoring.service.d.ts +12 -0
- package/dist/services/risk-scoring.service.d.ts.map +1 -0
- package/dist/services/risk-scoring.service.js +44 -0
- package/dist/services/risk-scoring.service.js.map +1 -0
- package/dist/services/session.service.d.ts +64 -0
- package/dist/services/session.service.d.ts.map +1 -0
- package/dist/services/session.service.js +455 -0
- package/dist/services/session.service.js.map +1 -0
- package/dist/services/social-auth-base.service.d.ts +57 -0
- package/dist/services/social-auth-base.service.d.ts.map +1 -0
- package/dist/services/social-auth-base.service.js +340 -0
- package/dist/services/social-auth-base.service.js.map +1 -0
- package/dist/services/social-auth.service.d.ts +31 -0
- package/dist/services/social-auth.service.d.ts.map +1 -0
- package/dist/services/social-auth.service.js +172 -0
- package/dist/services/social-auth.service.js.map +1 -0
- package/dist/services/social-provider-registry.service.d.ts +9 -0
- package/dist/services/social-provider-registry.service.d.ts.map +1 -0
- package/dist/services/social-provider-registry.service.js +30 -0
- package/dist/services/social-provider-registry.service.js.map +1 -0
- package/dist/services/trusted-device.service.d.ts +29 -0
- package/dist/services/trusted-device.service.d.ts.map +1 -0
- package/dist/services/trusted-device.service.js +190 -0
- package/dist/services/trusted-device.service.js.map +1 -0
- package/dist/storage/account-lockout-storage.service.d.ts +16 -0
- package/dist/storage/account-lockout-storage.service.d.ts.map +1 -0
- package/dist/storage/account-lockout-storage.service.js +50 -0
- package/dist/storage/account-lockout-storage.service.js.map +1 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +20 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/memory-storage.adapter.d.ts +33 -0
- package/dist/storage/memory-storage.adapter.d.ts.map +1 -0
- package/dist/storage/memory-storage.adapter.js +195 -0
- package/dist/storage/memory-storage.adapter.js.map +1 -0
- package/dist/storage/rate-limit-storage.service.d.ts +11 -0
- package/dist/storage/rate-limit-storage.service.d.ts.map +1 -0
- package/dist/storage/rate-limit-storage.service.js +33 -0
- package/dist/storage/rate-limit-storage.service.js.map +1 -0
- package/dist/templates/html-template.engine.d.ts +16 -0
- package/dist/templates/html-template.engine.d.ts.map +1 -0
- package/dist/templates/html-template.engine.js +502 -0
- package/dist/templates/html-template.engine.js.map +1 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +18 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/utils/common-passwords.d.ts +4 -0
- package/dist/utils/common-passwords.d.ts.map +1 -0
- package/dist/utils/common-passwords.js +108 -0
- package/dist/utils/common-passwords.js.map +1 -0
- package/dist/utils/context-storage.d.ts +13 -0
- package/dist/utils/context-storage.d.ts.map +1 -0
- package/dist/utils/context-storage.js +54 -0
- package/dist/utils/context-storage.js.map +1 -0
- package/dist/utils/cookie-names.util.d.ts +7 -0
- package/dist/utils/cookie-names.util.d.ts.map +1 -0
- package/dist/utils/cookie-names.util.js +30 -0
- package/dist/utils/cookie-names.util.js.map +1 -0
- package/dist/utils/cookies.util.d.ts +12 -0
- package/dist/utils/cookies.util.d.ts.map +1 -0
- package/dist/utils/cookies.util.js +48 -0
- package/dist/utils/cookies.util.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +24 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/ip-extractor.d.ts +12 -0
- package/dist/utils/ip-extractor.d.ts.map +1 -0
- package/dist/utils/ip-extractor.js +88 -0
- package/dist/utils/ip-extractor.js.map +1 -0
- package/dist/utils/nauth-logger.d.ts +20 -0
- package/dist/utils/nauth-logger.d.ts.map +1 -0
- package/dist/utils/nauth-logger.js +129 -0
- package/dist/utils/nauth-logger.js.map +1 -0
- package/dist/utils/pii-redactor.d.ts +16 -0
- package/dist/utils/pii-redactor.d.ts.map +1 -0
- package/dist/utils/pii-redactor.js +147 -0
- package/dist/utils/pii-redactor.js.map +1 -0
- package/dist/utils/setup/get-repositories.d.ts +16 -0
- package/dist/utils/setup/get-repositories.d.ts.map +1 -0
- package/dist/utils/setup/get-repositories.js +36 -0
- package/dist/utils/setup/get-repositories.js.map +1 -0
- package/dist/utils/setup/init-services.d.ts +41 -0
- package/dist/utils/setup/init-services.d.ts.map +1 -0
- package/dist/utils/setup/init-services.js +107 -0
- package/dist/utils/setup/init-services.js.map +1 -0
- package/dist/utils/setup/init-social.d.ts +13 -0
- package/dist/utils/setup/init-social.d.ts.map +1 -0
- package/dist/utils/setup/init-social.js +77 -0
- package/dist/utils/setup/init-social.js.map +1 -0
- package/dist/utils/setup/init-storage.d.ts +4 -0
- package/dist/utils/setup/init-storage.d.ts.map +1 -0
- package/dist/utils/setup/init-storage.js +79 -0
- package/dist/utils/setup/init-storage.js.map +1 -0
- package/dist/utils/setup/register-mfa.d.ts +5 -0
- package/dist/utils/setup/register-mfa.d.ts.map +1 -0
- package/dist/utils/setup/register-mfa.js +85 -0
- package/dist/utils/setup/register-mfa.js.map +1 -0
- package/dist/utils/setup/run-nauth-migrations.d.ts +5 -0
- package/dist/utils/setup/run-nauth-migrations.d.ts.map +1 -0
- package/dist/utils/setup/run-nauth-migrations.js +67 -0
- package/dist/utils/setup/run-nauth-migrations.js.map +1 -0
- package/dist/utils/token-delivery-policy.d.ts +6 -0
- package/dist/utils/token-delivery-policy.d.ts.map +1 -0
- package/dist/utils/token-delivery-policy.js +15 -0
- package/dist/utils/token-delivery-policy.js.map +1 -0
- package/dist/validators/template.validator.d.ts +7 -0
- package/dist/validators/template.validator.d.ts.map +1 -0
- package/dist/validators/template.validator.js +95 -0
- package/dist/validators/template.validator.js.map +1 -0
- package/jest.config.js +15 -0
- package/jest.setup.ts +6 -0
- package/package.json +73 -0
- package/src/adapters/database-columns.ts +165 -0
- package/src/adapters/express.adapter.ts +385 -0
- package/src/adapters/fastify.adapter.ts +416 -0
- package/src/adapters/index.ts +16 -0
- package/src/adapters/storage.factory.ts +143 -0
- package/src/bootstrap.ts +374 -0
- package/src/dto/auth-challenge.dto.ts +231 -0
- package/src/dto/auth-response.dto.ts +253 -0
- package/src/dto/challenge-response.dto.ts +234 -0
- package/src/dto/change-password-request.dto.ts +50 -0
- package/src/dto/change-password-response.dto.ts +29 -0
- package/src/dto/change-password.dto.ts +57 -0
- package/src/dto/error-response.dto.ts +136 -0
- package/src/dto/get-available-methods.dto.ts +55 -0
- package/src/dto/get-challenge-data-response.dto.ts +28 -0
- package/src/dto/get-challenge-data.dto.ts +69 -0
- package/src/dto/get-client-info.dto.ts +104 -0
- package/src/dto/get-device-token-response.dto.ts +25 -0
- package/src/dto/get-events-by-type.dto.ts +76 -0
- package/src/dto/get-ip-address-response.dto.ts +24 -0
- package/src/dto/get-mfa-status.dto.ts +94 -0
- package/src/dto/get-risk-assessment-history.dto.ts +39 -0
- package/src/dto/get-session-id-response.dto.ts +25 -0
- package/src/dto/get-setup-data-response.dto.ts +31 -0
- package/src/dto/get-setup-data.dto.ts +75 -0
- package/src/dto/get-suspicious-activity.dto.ts +42 -0
- package/src/dto/get-user-agent-response.dto.ts +23 -0
- package/src/dto/get-user-auth-history.dto.ts +95 -0
- package/src/dto/get-user-by-email.dto.ts +61 -0
- package/src/dto/get-user-by-id.dto.ts +46 -0
- package/src/dto/get-user-devices.dto.ts +53 -0
- package/src/dto/get-user-response.dto.ts +17 -0
- package/src/dto/has-provider.dto.ts +56 -0
- package/src/dto/index.ts +57 -0
- package/src/dto/is-trusted-device-response.dto.ts +34 -0
- package/src/dto/list-providers-response.dto.ts +23 -0
- package/src/dto/login.dto.ts +95 -0
- package/src/dto/logout-all-response.dto.ts +24 -0
- package/src/dto/logout-all.dto.ts +65 -0
- package/src/dto/logout-response.dto.ts +25 -0
- package/src/dto/logout.dto.ts +64 -0
- package/src/dto/refresh-token.dto.ts +36 -0
- package/src/dto/remove-devices.dto.ts +85 -0
- package/src/dto/resend-code-response.dto.ts +32 -0
- package/src/dto/resend-code.dto.ts +51 -0
- package/src/dto/reset-password.dto.ts +115 -0
- package/src/dto/respond-challenge.dto.ts +272 -0
- package/src/dto/set-mfa-exemption.dto.ts +112 -0
- package/src/dto/set-must-change-password-response.dto.ts +27 -0
- package/src/dto/set-must-change-password.dto.ts +46 -0
- package/src/dto/set-preferred-method.dto.ts +80 -0
- package/src/dto/setup-mfa.dto.ts +98 -0
- package/src/dto/signup.dto.ts +174 -0
- package/src/dto/social-auth.dto.ts +422 -0
- package/src/dto/trust-device-response.dto.ts +30 -0
- package/src/dto/trust-device.dto.ts +9 -0
- package/src/dto/update-user-attributes-request.dto.ts +51 -0
- package/src/dto/user-response.dto.ts +138 -0
- package/src/dto/user-update.dto.ts +222 -0
- package/src/dto/verify-email.dto.ts +313 -0
- package/src/dto/verify-mfa-code.dto.ts +103 -0
- package/src/dto/verify-phone-by-sub.dto.ts +78 -0
- package/src/dto/verify-phone.dto.ts +245 -0
- package/src/entities/auth-audit.entity.ts +232 -0
- package/src/entities/challenge-session.entity.ts +116 -0
- package/src/entities/index.ts +29 -0
- package/src/entities/login-attempt.entity.ts +64 -0
- package/src/entities/mfa-device.entity.ts +151 -0
- package/src/entities/rate-limit.entity.ts +44 -0
- package/src/entities/session.entity.ts +180 -0
- package/src/entities/social-account.entity.ts +96 -0
- package/src/entities/storage-lock.entity.ts +39 -0
- package/src/entities/trusted-device.entity.ts +112 -0
- package/src/entities/user.entity.ts +243 -0
- package/src/entities/verification-token.entity.ts +141 -0
- package/src/enums/auth-audit-event-type.enum.ts +360 -0
- package/src/enums/error-codes.enum.ts +420 -0
- package/src/enums/mfa-method.enum.ts +97 -0
- package/src/enums/risk-factor.enum.ts +111 -0
- package/src/exceptions/nauth.exception.ts +231 -0
- package/src/handlers/auth.handler.ts +260 -0
- package/src/handlers/client-info.handler.ts +101 -0
- package/src/handlers/csrf.handler.ts +156 -0
- package/src/handlers/token-delivery.handler.ts +118 -0
- package/src/index.ts +118 -0
- package/src/interfaces/client-info.interface.ts +85 -0
- package/src/interfaces/config.interface.ts +2135 -0
- package/src/interfaces/entities.interface.ts +226 -0
- package/src/interfaces/index.ts +15 -0
- package/src/interfaces/logger.interface.ts +283 -0
- package/src/interfaces/mfa-provider.interface.ts +154 -0
- package/src/interfaces/oauth.interface.ts +148 -0
- package/src/interfaces/provider.interface.ts +47 -0
- package/src/interfaces/social-auth-provider.interface.ts +131 -0
- package/src/interfaces/storage-adapter.interface.ts +82 -0
- package/src/interfaces/template.interface.ts +510 -0
- package/src/interfaces/token-verifier.interface.ts +110 -0
- package/src/internal.ts +178 -0
- package/src/platform/interfaces.ts +299 -0
- package/src/schemas/auth-config.schema.ts +646 -0
- package/src/services/adaptive-mfa-decision.service.spec.ts +1058 -0
- package/src/services/adaptive-mfa-decision.service.ts +457 -0
- package/src/services/auth-audit.service.spec.ts +675 -0
- package/src/services/auth-audit.service.ts +558 -0
- package/src/services/auth-challenge-helper.service.spec.ts +3227 -0
- package/src/services/auth-challenge-helper.service.ts +825 -0
- package/src/services/auth-flow-context-builder.service.ts +520 -0
- package/src/services/auth-flow-rules.ts +202 -0
- package/src/services/auth-flow-state-definitions.ts +190 -0
- package/src/services/auth-flow-state-machine.service.ts +207 -0
- package/src/services/auth-flow-state-machine.types.ts +316 -0
- package/src/services/auth.service.spec.ts +4195 -0
- package/src/services/auth.service.ts +3727 -0
- package/src/services/challenge.service.spec.ts +1363 -0
- package/src/services/challenge.service.ts +696 -0
- package/src/services/client-info.service.spec.ts +572 -0
- package/src/services/client-info.service.ts +374 -0
- package/src/services/csrf.service.ts +54 -0
- package/src/services/email-verification.service.spec.ts +1229 -0
- package/src/services/email-verification.service.ts +578 -0
- package/src/services/geo-location.service.spec.ts +603 -0
- package/src/services/geo-location.service.ts +599 -0
- package/src/services/index.ts +13 -0
- package/src/services/jwt.service.spec.ts +882 -0
- package/src/services/jwt.service.ts +621 -0
- package/src/services/mfa-base.service.spec.ts +246 -0
- package/src/services/mfa-base.service.ts +611 -0
- package/src/services/mfa.service.spec.ts +693 -0
- package/src/services/mfa.service.ts +960 -0
- package/src/services/password.service.spec.ts +166 -0
- package/src/services/password.service.ts +309 -0
- package/src/services/phone-verification.service.spec.ts +1120 -0
- package/src/services/phone-verification.service.ts +751 -0
- package/src/services/risk-detection.service.spec.ts +1292 -0
- package/src/services/risk-detection.service.ts +1012 -0
- package/src/services/risk-scoring.service.spec.ts +204 -0
- package/src/services/risk-scoring.service.ts +131 -0
- package/src/services/session.service.spec.ts +1293 -0
- package/src/services/session.service.ts +803 -0
- package/src/services/social-account.service.spec.ts +725 -0
- package/src/services/social-auth-base.service.spec.ts +418 -0
- package/src/services/social-auth-base.service.ts +581 -0
- package/src/services/social-auth.service.spec.ts +238 -0
- package/src/services/social-auth.service.ts +436 -0
- package/src/services/social-provider-registry.service.spec.ts +238 -0
- package/src/services/social-provider-registry.service.ts +122 -0
- package/src/services/trusted-device.service.spec.ts +505 -0
- package/src/services/trusted-device.service.ts +339 -0
- package/src/storage/account-lockout-storage.service.spec.ts +310 -0
- package/src/storage/account-lockout-storage.service.ts +89 -0
- package/src/storage/index.ts +3 -0
- package/src/storage/memory-storage.adapter.ts +443 -0
- package/src/storage/rate-limit-storage.service.spec.ts +247 -0
- package/src/storage/rate-limit-storage.service.ts +38 -0
- package/src/templates/html-template.engine.spec.ts +161 -0
- package/src/templates/html-template.engine.ts +688 -0
- package/src/templates/index.ts +7 -0
- package/src/utils/common-passwords.spec.ts +230 -0
- package/src/utils/common-passwords.ts +170 -0
- package/src/utils/context-storage.ts +188 -0
- package/src/utils/cookie-names.util.ts +67 -0
- package/src/utils/cookies.util.ts +94 -0
- package/src/utils/index.ts +12 -0
- package/src/utils/ip-extractor.spec.ts +330 -0
- package/src/utils/ip-extractor.ts +220 -0
- package/src/utils/nauth-logger.spec.ts +388 -0
- package/src/utils/nauth-logger.ts +215 -0
- package/src/utils/pii-redactor.spec.ts +130 -0
- package/src/utils/pii-redactor.ts +288 -0
- package/src/utils/setup/get-repositories.ts +140 -0
- package/src/utils/setup/init-services.ts +422 -0
- package/src/utils/setup/init-social.ts +189 -0
- package/src/utils/setup/init-storage.ts +94 -0
- package/src/utils/setup/register-mfa.ts +165 -0
- package/src/utils/setup/run-nauth-migrations.ts +61 -0
- package/src/utils/token-delivery-policy.ts +38 -0
- package/src/validators/template.validator.ts +219 -0
- package/tsconfig.json +37 -0
- package/tsconfig.lint.json +6 -0
|
@@ -0,0 +1,1363 @@
|
|
|
1
|
+
import { Repository, SelectQueryBuilder } from 'typeorm';
|
|
2
|
+
import { ChallengeService } from './challenge.service';
|
|
3
|
+
import { NAuthException } from '../exceptions/nauth.exception';
|
|
4
|
+
import { IUser, IChallengeSession } from '../interfaces/entities.interface';
|
|
5
|
+
import { AuthChallenge } from '../dto/auth-challenge.dto';
|
|
6
|
+
import { NAuthLogger } from '../utils/nauth-logger';
|
|
7
|
+
import { AuthAuditService } from './auth-audit.service';
|
|
8
|
+
import { AuthAuditEventType } from '../enums/auth-audit-event-type.enum';
|
|
9
|
+
import { AuthErrorCode } from '../enums/error-codes.enum';
|
|
10
|
+
import { BaseChallengeSession } from '../entities';
|
|
11
|
+
import { ClientInfoService } from './client-info.service';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Challenge Service Unit Tests
|
|
15
|
+
*
|
|
16
|
+
* Tests challenge session creation, validation, consumption, and cleanup.
|
|
17
|
+
* Covers all challenge types, expiration, max attempts, and edge cases.
|
|
18
|
+
*
|
|
19
|
+
* Platform-agnostic: Uses direct instantiation, no NestJS dependencies.
|
|
20
|
+
*/
|
|
21
|
+
describe('ChallengeService', () => {
|
|
22
|
+
let service: ChallengeService;
|
|
23
|
+
let mockChallengeSessionRepository: jest.Mocked<Repository<BaseChallengeSession>>;
|
|
24
|
+
let mockClientInfoService: jest.Mocked<ClientInfoService>;
|
|
25
|
+
let mockAuditService: jest.Mocked<AuthAuditService>;
|
|
26
|
+
let mockLogger: jest.Mocked<NAuthLogger>;
|
|
27
|
+
let mockQueryBuilder: jest.Mocked<SelectQueryBuilder<BaseChallengeSession>>;
|
|
28
|
+
|
|
29
|
+
const mockUser: Partial<IUser> = {
|
|
30
|
+
id: 1,
|
|
31
|
+
sub: 'user-uuid-123',
|
|
32
|
+
email: 'test@example.com',
|
|
33
|
+
phone: '+1234567890',
|
|
34
|
+
isEmailVerified: false,
|
|
35
|
+
isPhoneVerified: false,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const mockChallengeSession: Partial<IChallengeSession> = {
|
|
39
|
+
id: 1,
|
|
40
|
+
userId: 1,
|
|
41
|
+
user: mockUser as IUser,
|
|
42
|
+
challengeName: AuthChallenge.VERIFY_EMAIL,
|
|
43
|
+
sessionToken: 'session-token-123',
|
|
44
|
+
expiresAt: new Date(Date.now() + 15 * 60 * 1000), // 15 minutes from now
|
|
45
|
+
isCompleted: false,
|
|
46
|
+
completedAt: null,
|
|
47
|
+
attempts: 0,
|
|
48
|
+
maxAttempts: 3,
|
|
49
|
+
ipAddress: '1.2.3.4',
|
|
50
|
+
userAgent: 'test-user-agent',
|
|
51
|
+
createdAt: new Date(),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
// Create mock query builder
|
|
56
|
+
mockQueryBuilder = {
|
|
57
|
+
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
|
58
|
+
where: jest.fn().mockReturnThis(),
|
|
59
|
+
select: jest.fn().mockReturnThis(),
|
|
60
|
+
getOne: jest.fn(),
|
|
61
|
+
} as any;
|
|
62
|
+
|
|
63
|
+
// Create mock repository
|
|
64
|
+
mockChallengeSessionRepository = {
|
|
65
|
+
create: jest.fn(),
|
|
66
|
+
save: jest.fn(),
|
|
67
|
+
delete: jest.fn(),
|
|
68
|
+
findOne: jest.fn(),
|
|
69
|
+
createQueryBuilder: jest.fn(() => mockQueryBuilder),
|
|
70
|
+
} as any;
|
|
71
|
+
|
|
72
|
+
// Create mock services
|
|
73
|
+
|
|
74
|
+
mockAuditService = {
|
|
75
|
+
recordEvent: jest.fn(),
|
|
76
|
+
} as any;
|
|
77
|
+
|
|
78
|
+
mockLogger = {
|
|
79
|
+
log: jest.fn(),
|
|
80
|
+
error: jest.fn(),
|
|
81
|
+
warn: jest.fn(),
|
|
82
|
+
debug: jest.fn(),
|
|
83
|
+
} as any;
|
|
84
|
+
|
|
85
|
+
mockClientInfoService = {
|
|
86
|
+
get: jest.fn().mockReturnValue({
|
|
87
|
+
ipAddress: '1.2.3.4',
|
|
88
|
+
userAgent: 'test-user-agent',
|
|
89
|
+
}),
|
|
90
|
+
} as any;
|
|
91
|
+
|
|
92
|
+
// Instantiate service directly
|
|
93
|
+
service = new ChallengeService(mockChallengeSessionRepository, mockClientInfoService, mockLogger, mockAuditService);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
jest.clearAllMocks();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Service Initialization
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
it('should be defined', () => {
|
|
105
|
+
expect(service).toBeDefined();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// createChallengeSession() Method
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
describe('createChallengeSession', () => {
|
|
113
|
+
it('should create a challenge session successfully', async () => {
|
|
114
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
115
|
+
mockChallengeSessionRepository.create.mockReturnValue(mockChallengeSession as any);
|
|
116
|
+
mockChallengeSessionRepository.save.mockResolvedValue(mockChallengeSession as any);
|
|
117
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
118
|
+
|
|
119
|
+
const result = await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL, {
|
|
120
|
+
email: mockUser.email,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result).toBeDefined();
|
|
124
|
+
expect(result.challengeName).toBe(AuthChallenge.VERIFY_EMAIL);
|
|
125
|
+
expect(mockChallengeSessionRepository.create).toHaveBeenCalled();
|
|
126
|
+
expect(mockChallengeSessionRepository.save).toHaveBeenCalled();
|
|
127
|
+
expect(mockLogger.log).toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should cleanup expired sessions before creating new one', async () => {
|
|
131
|
+
const deleteSpy = mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 2 } as any);
|
|
132
|
+
mockChallengeSessionRepository.create.mockReturnValue(mockChallengeSession as any);
|
|
133
|
+
mockChallengeSessionRepository.save.mockResolvedValue(mockChallengeSession as any);
|
|
134
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
135
|
+
|
|
136
|
+
await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL);
|
|
137
|
+
|
|
138
|
+
expect(deleteSpy).toHaveBeenCalledTimes(2); // Once for expired, once for completed
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should throttle cleanup to once per 5 minutes per user', async () => {
|
|
142
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
143
|
+
mockChallengeSessionRepository.create.mockReturnValue(mockChallengeSession as any);
|
|
144
|
+
mockChallengeSessionRepository.save.mockResolvedValue(mockChallengeSession as any);
|
|
145
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
146
|
+
|
|
147
|
+
// Create first session
|
|
148
|
+
await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL);
|
|
149
|
+
const firstDeleteCount = mockChallengeSessionRepository.delete.mock.calls.length;
|
|
150
|
+
|
|
151
|
+
// Create second session immediately (should not trigger cleanup again)
|
|
152
|
+
await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_PHONE);
|
|
153
|
+
const secondDeleteCount = mockChallengeSessionRepository.delete.mock.calls.length;
|
|
154
|
+
|
|
155
|
+
// Cleanup should only run once (first call)
|
|
156
|
+
expect(secondDeleteCount).toBe(firstDeleteCount);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should create session with default expiration (15 minutes)', async () => {
|
|
160
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
161
|
+
const createdSession = { ...mockChallengeSession, expiresAt: new Date() } as any;
|
|
162
|
+
mockChallengeSessionRepository.create.mockReturnValue(createdSession);
|
|
163
|
+
mockChallengeSessionRepository.save.mockResolvedValue(createdSession);
|
|
164
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
165
|
+
|
|
166
|
+
jest.useFakeTimers();
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
jest.setSystemTime(now);
|
|
169
|
+
|
|
170
|
+
await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL);
|
|
171
|
+
|
|
172
|
+
expect(mockChallengeSessionRepository.create).toHaveBeenCalledWith(
|
|
173
|
+
(expect as any).objectContaining({
|
|
174
|
+
expiresAt: (expect as any).any(Date),
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const createdCall = mockChallengeSessionRepository.create.mock.calls[0][0] as any;
|
|
179
|
+
const expectedExpiry = new Date(now + 15 * 60 * 1000);
|
|
180
|
+
if (createdCall?.expiresAt) {
|
|
181
|
+
expect(createdCall.expiresAt.getTime()).toBeCloseTo(expectedExpiry.getTime(), -2); // Within 100ms
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
jest.useRealTimers();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should create session with provided metadata', async () => {
|
|
188
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
189
|
+
mockChallengeSessionRepository.create.mockReturnValue(mockChallengeSession as any);
|
|
190
|
+
mockChallengeSessionRepository.save.mockResolvedValue(mockChallengeSession as any);
|
|
191
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
192
|
+
|
|
193
|
+
const metadata = { email: 'test@example.com', verificationTokenId: 123 };
|
|
194
|
+
|
|
195
|
+
await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL, metadata);
|
|
196
|
+
|
|
197
|
+
expect(mockChallengeSessionRepository.create).toHaveBeenCalledWith(
|
|
198
|
+
(expect as any).objectContaining({
|
|
199
|
+
metadata,
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should create session with provided IP and user agent', async () => {
|
|
205
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
206
|
+
mockChallengeSessionRepository.create.mockReturnValue(mockChallengeSession as any);
|
|
207
|
+
mockChallengeSessionRepository.save.mockResolvedValue(mockChallengeSession as any);
|
|
208
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
209
|
+
|
|
210
|
+
mockClientInfoService.get.mockReturnValue({
|
|
211
|
+
ipAddress: '192.168.1.1',
|
|
212
|
+
userAgent: 'Custom-Agent',
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL, undefined);
|
|
216
|
+
|
|
217
|
+
expect(mockChallengeSessionRepository.create).toHaveBeenCalledWith(
|
|
218
|
+
(expect as any).objectContaining({
|
|
219
|
+
ipAddress: '192.168.1.1',
|
|
220
|
+
userAgent: 'Custom-Agent',
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should record audit event on session creation', async () => {
|
|
226
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
227
|
+
const sessionWithId = { ...mockChallengeSession, id: 1 } as any;
|
|
228
|
+
mockChallengeSessionRepository.create.mockReturnValue(sessionWithId);
|
|
229
|
+
mockChallengeSessionRepository.save.mockResolvedValue(sessionWithId);
|
|
230
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
231
|
+
|
|
232
|
+
await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL);
|
|
233
|
+
|
|
234
|
+
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
235
|
+
(expect as any).objectContaining({
|
|
236
|
+
eventType: AuthAuditEventType.CHALLENGE_CREATED,
|
|
237
|
+
eventStatus: 'INFO',
|
|
238
|
+
userId: mockUser.id,
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should handle audit service errors gracefully', async () => {
|
|
244
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
245
|
+
mockChallengeSessionRepository.create.mockReturnValue(mockChallengeSession as any);
|
|
246
|
+
mockChallengeSessionRepository.save.mockResolvedValue(mockChallengeSession as any);
|
|
247
|
+
mockAuditService.recordEvent.mockRejectedValue(new Error('Audit service error'));
|
|
248
|
+
|
|
249
|
+
const result = await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL);
|
|
250
|
+
|
|
251
|
+
expect(result).toBeDefined();
|
|
252
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should handle non-Error audit exceptions', async () => {
|
|
256
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
257
|
+
mockChallengeSessionRepository.create.mockReturnValue(mockChallengeSession as any);
|
|
258
|
+
mockChallengeSessionRepository.save.mockResolvedValue(mockChallengeSession as any);
|
|
259
|
+
mockAuditService.recordEvent.mockRejectedValue('String error' as any);
|
|
260
|
+
|
|
261
|
+
const result = await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL);
|
|
262
|
+
|
|
263
|
+
expect(result).toBeDefined();
|
|
264
|
+
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
265
|
+
(expect as any).stringContaining('Failed to record CHALLENGE_CREATED audit event: Unknown error'),
|
|
266
|
+
(expect as any).any(Object),
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should reuse existing active challenge session (deduplication)', async () => {
|
|
271
|
+
const existingSession = {
|
|
272
|
+
...mockChallengeSession,
|
|
273
|
+
id: 123,
|
|
274
|
+
sessionToken: 'existing-token-456',
|
|
275
|
+
expiresAt: new Date(Date.now() + 10 * 60 * 1000), // 10 minutes from now (not expired)
|
|
276
|
+
isCompleted: false,
|
|
277
|
+
user: mockUser,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Mock finding an existing active session
|
|
281
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(existingSession as any);
|
|
282
|
+
|
|
283
|
+
const result = await service.createChallengeSession(mockUser as IUser, AuthChallenge.MFA_SETUP_REQUIRED);
|
|
284
|
+
|
|
285
|
+
// Should return existing session
|
|
286
|
+
expect(result.sessionToken).toBe('existing-token-456');
|
|
287
|
+
expect(result.id).toBe(123);
|
|
288
|
+
|
|
289
|
+
// Should NOT create a new session
|
|
290
|
+
expect(mockChallengeSessionRepository.create).not.toHaveBeenCalled();
|
|
291
|
+
expect(mockChallengeSessionRepository.save).not.toHaveBeenCalled();
|
|
292
|
+
|
|
293
|
+
// Should NOT record a duplicate CHALLENGE_CREATED audit event
|
|
294
|
+
expect(mockAuditService.recordEvent).not.toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should create new session if existing session is expired', async () => {
|
|
298
|
+
const expiredSession = {
|
|
299
|
+
...mockChallengeSession,
|
|
300
|
+
id: 123,
|
|
301
|
+
sessionToken: 'expired-token',
|
|
302
|
+
expiresAt: new Date(Date.now() - 1000), // Expired 1 second ago
|
|
303
|
+
isCompleted: false,
|
|
304
|
+
user: mockUser,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Mock finding an expired session
|
|
308
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(expiredSession as any);
|
|
309
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 1 } as any);
|
|
310
|
+
mockChallengeSessionRepository.create.mockReturnValue(mockChallengeSession as any);
|
|
311
|
+
mockChallengeSessionRepository.save.mockResolvedValue({ ...mockChallengeSession, id: 124 } as any);
|
|
312
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
313
|
+
|
|
314
|
+
const result = await service.createChallengeSession(mockUser as IUser, AuthChallenge.MFA_SETUP_REQUIRED);
|
|
315
|
+
|
|
316
|
+
// Should delete expired session
|
|
317
|
+
expect(mockChallengeSessionRepository.delete).toHaveBeenCalledWith({ id: 123 });
|
|
318
|
+
|
|
319
|
+
// Should create a new session
|
|
320
|
+
expect(mockChallengeSessionRepository.create).toHaveBeenCalled();
|
|
321
|
+
expect(mockChallengeSessionRepository.save).toHaveBeenCalled();
|
|
322
|
+
|
|
323
|
+
// Should record new CHALLENGE_CREATED audit event
|
|
324
|
+
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
325
|
+
(expect as any).objectContaining({
|
|
326
|
+
eventType: AuthAuditEventType.CHALLENGE_CREATED,
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should create new session if no existing session found', async () => {
|
|
332
|
+
// Mock no existing session
|
|
333
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(null);
|
|
334
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
335
|
+
mockChallengeSessionRepository.create.mockReturnValue(mockChallengeSession as any);
|
|
336
|
+
mockChallengeSessionRepository.save.mockResolvedValue({ ...mockChallengeSession, id: 125 } as any);
|
|
337
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
338
|
+
|
|
339
|
+
const result = await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL);
|
|
340
|
+
|
|
341
|
+
// Should create a new session
|
|
342
|
+
expect(mockChallengeSessionRepository.create).toHaveBeenCalled();
|
|
343
|
+
expect(mockChallengeSessionRepository.save).toHaveBeenCalled();
|
|
344
|
+
|
|
345
|
+
// Should record CHALLENGE_CREATED audit event
|
|
346
|
+
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
347
|
+
(expect as any).objectContaining({
|
|
348
|
+
eventType: AuthAuditEventType.CHALLENGE_CREATED,
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should use client info from ClientInfoService in audit event', async () => {
|
|
354
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(null);
|
|
355
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
356
|
+
const sessionWithId = { ...mockChallengeSession, id: 1 } as any;
|
|
357
|
+
mockChallengeSessionRepository.create.mockReturnValue(sessionWithId);
|
|
358
|
+
mockChallengeSessionRepository.save.mockResolvedValue(sessionWithId);
|
|
359
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
360
|
+
|
|
361
|
+
mockClientInfoService.get.mockReturnValue({
|
|
362
|
+
ipAddress: '192.168.1.100',
|
|
363
|
+
userAgent: 'Custom-Agent-String',
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL, undefined);
|
|
367
|
+
|
|
368
|
+
// Audit service now gets IP and userAgent from ClientInfoService automatically
|
|
369
|
+
// Verify that recordEvent was called (the audit service will extract client info from context)
|
|
370
|
+
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
371
|
+
(expect as any).objectContaining({
|
|
372
|
+
userId: mockUser.id,
|
|
373
|
+
eventType: AuthAuditEventType.CHALLENGE_CREATED,
|
|
374
|
+
eventStatus: 'INFO',
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
// Verify that ClientInfoService was called to get client info
|
|
378
|
+
expect(mockClientInfoService.get).toHaveBeenCalled();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should create MFA challenge sessions with metadata', async () => {
|
|
382
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
383
|
+
const mfaSession = {
|
|
384
|
+
...mockChallengeSession,
|
|
385
|
+
challengeName: AuthChallenge.MFA_REQUIRED,
|
|
386
|
+
} as any;
|
|
387
|
+
mockChallengeSessionRepository.create.mockReturnValue(mfaSession);
|
|
388
|
+
mockChallengeSessionRepository.save.mockResolvedValue(mfaSession);
|
|
389
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
390
|
+
|
|
391
|
+
const mfaMetadata = {
|
|
392
|
+
deviceId: 'device-123',
|
|
393
|
+
method: 'TOTP',
|
|
394
|
+
availableMethods: ['TOTP', 'SMS'],
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const result = await service.createChallengeSession(mockUser as IUser, AuthChallenge.MFA_REQUIRED, mfaMetadata);
|
|
398
|
+
|
|
399
|
+
expect(result.challengeName).toBe(AuthChallenge.MFA_REQUIRED);
|
|
400
|
+
expect(mockChallengeSessionRepository.create).toHaveBeenCalledWith(
|
|
401
|
+
(expect as any).objectContaining({
|
|
402
|
+
challengeName: AuthChallenge.MFA_REQUIRED,
|
|
403
|
+
metadata: mfaMetadata,
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should create MFA_SETUP_REQUIRED challenge sessions', async () => {
|
|
409
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
410
|
+
const mfaSetupSession = {
|
|
411
|
+
...mockChallengeSession,
|
|
412
|
+
challengeName: AuthChallenge.MFA_SETUP_REQUIRED,
|
|
413
|
+
} as any;
|
|
414
|
+
mockChallengeSessionRepository.create.mockReturnValue(mfaSetupSession);
|
|
415
|
+
mockChallengeSessionRepository.save.mockResolvedValue(mfaSetupSession);
|
|
416
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
417
|
+
|
|
418
|
+
const setupMetadata = {
|
|
419
|
+
allowedMethods: ['TOTP', 'SMS', 'EMAIL'],
|
|
420
|
+
gracePeriodExpired: true,
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const result = await service.createChallengeSession(
|
|
424
|
+
mockUser as IUser,
|
|
425
|
+
AuthChallenge.MFA_SETUP_REQUIRED,
|
|
426
|
+
setupMetadata,
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
expect(result.challengeName).toBe(AuthChallenge.MFA_SETUP_REQUIRED);
|
|
430
|
+
expect(mockChallengeSessionRepository.create).toHaveBeenCalledWith(
|
|
431
|
+
(expect as any).objectContaining({
|
|
432
|
+
challengeName: AuthChallenge.MFA_SETUP_REQUIRED,
|
|
433
|
+
metadata: setupMetadata,
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('should create FORCE_CHANGE_PASSWORD challenge sessions', async () => {
|
|
439
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
440
|
+
const passwordChangeSession = {
|
|
441
|
+
...mockChallengeSession,
|
|
442
|
+
challengeName: AuthChallenge.FORCE_CHANGE_PASSWORD,
|
|
443
|
+
} as any;
|
|
444
|
+
mockChallengeSessionRepository.create.mockReturnValue(passwordChangeSession);
|
|
445
|
+
mockChallengeSessionRepository.save.mockResolvedValue(passwordChangeSession);
|
|
446
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
447
|
+
|
|
448
|
+
const passwordChangeMetadata = {
|
|
449
|
+
reason: 'admin_forced',
|
|
450
|
+
passwordExpired: true,
|
|
451
|
+
instructions: 'You must change your password before continuing',
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const result = await service.createChallengeSession(
|
|
455
|
+
mockUser as IUser,
|
|
456
|
+
AuthChallenge.FORCE_CHANGE_PASSWORD,
|
|
457
|
+
passwordChangeMetadata,
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
expect(result.challengeName).toBe(AuthChallenge.FORCE_CHANGE_PASSWORD);
|
|
461
|
+
expect(mockChallengeSessionRepository.create).toHaveBeenCalledWith(
|
|
462
|
+
(expect as any).objectContaining({
|
|
463
|
+
challengeName: AuthChallenge.FORCE_CHANGE_PASSWORD,
|
|
464
|
+
metadata: passwordChangeMetadata,
|
|
465
|
+
}),
|
|
466
|
+
);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// VERIFY_EMAIL_AND_PHONE removed - challenges are sequential (VERIFY_EMAIL first, then VERIFY_PHONE)
|
|
470
|
+
// This test is no longer needed as the challenge system works sequentially
|
|
471
|
+
|
|
472
|
+
it('should trigger cleanup after 5 minutes have passed', async () => {
|
|
473
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
474
|
+
mockChallengeSessionRepository.create.mockReturnValue(mockChallengeSession as any);
|
|
475
|
+
mockChallengeSessionRepository.save.mockResolvedValue(mockChallengeSession as any);
|
|
476
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
477
|
+
|
|
478
|
+
jest.useFakeTimers();
|
|
479
|
+
const now = Date.now();
|
|
480
|
+
jest.setSystemTime(now);
|
|
481
|
+
|
|
482
|
+
// Create first session
|
|
483
|
+
await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL);
|
|
484
|
+
const firstDeleteCount = mockChallengeSessionRepository.delete.mock.calls.length;
|
|
485
|
+
|
|
486
|
+
// Advance time by 5 minutes and 1 second
|
|
487
|
+
jest.setSystemTime(now + 5 * 60 * 1000 + 1000);
|
|
488
|
+
|
|
489
|
+
// Create second session (should trigger cleanup again)
|
|
490
|
+
await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_PHONE);
|
|
491
|
+
const secondDeleteCount = mockChallengeSessionRepository.delete.mock.calls.length;
|
|
492
|
+
|
|
493
|
+
// Cleanup should run again after 5 minutes
|
|
494
|
+
expect(secondDeleteCount).toBeGreaterThan(firstDeleteCount);
|
|
495
|
+
|
|
496
|
+
jest.useRealTimers();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should create session for all challenge types', async () => {
|
|
500
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
501
|
+
mockChallengeSessionRepository.create.mockReturnValue(mockChallengeSession as any);
|
|
502
|
+
mockChallengeSessionRepository.save.mockResolvedValue(mockChallengeSession as any);
|
|
503
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
504
|
+
|
|
505
|
+
const challengeTypes = [
|
|
506
|
+
AuthChallenge.VERIFY_EMAIL,
|
|
507
|
+
AuthChallenge.VERIFY_PHONE,
|
|
508
|
+
AuthChallenge.MFA_REQUIRED,
|
|
509
|
+
AuthChallenge.MFA_SETUP_REQUIRED,
|
|
510
|
+
AuthChallenge.FORCE_CHANGE_PASSWORD,
|
|
511
|
+
];
|
|
512
|
+
|
|
513
|
+
for (const challengeType of challengeTypes) {
|
|
514
|
+
await service.createChallengeSession(mockUser as IUser, challengeType);
|
|
515
|
+
expect(mockChallengeSessionRepository.create).toHaveBeenCalledWith(
|
|
516
|
+
(expect as any).objectContaining({
|
|
517
|
+
challengeName: challengeType,
|
|
518
|
+
}),
|
|
519
|
+
);
|
|
520
|
+
jest.clearAllMocks();
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// ============================================================================
|
|
526
|
+
// validateSession() Method
|
|
527
|
+
// ============================================================================
|
|
528
|
+
|
|
529
|
+
describe('validateSession', () => {
|
|
530
|
+
it('should validate a valid session', async () => {
|
|
531
|
+
const validSession = {
|
|
532
|
+
...mockChallengeSession,
|
|
533
|
+
expiresAt: new Date(Date.now() + 60000), // 1 minute from now
|
|
534
|
+
};
|
|
535
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(validSession as any);
|
|
536
|
+
|
|
537
|
+
const result = await service.validateSession('session-token-123', AuthChallenge.VERIFY_EMAIL);
|
|
538
|
+
|
|
539
|
+
expect(result).toBeDefined();
|
|
540
|
+
expect(result.sessionToken).toBe('session-token-123');
|
|
541
|
+
expect(mockChallengeSessionRepository.findOne).toHaveBeenCalledWith({
|
|
542
|
+
where: { sessionToken: 'session-token-123' },
|
|
543
|
+
relations: ['user'],
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('should throw NAuthException if session not found', async () => {
|
|
548
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(null);
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
await service.validateSession('invalid-token');
|
|
552
|
+
fail('Should have thrown NAuthException');
|
|
553
|
+
} catch (error) {
|
|
554
|
+
expect(error).toBeInstanceOf(NAuthException);
|
|
555
|
+
expect((error as NAuthException).code).toBe(AuthErrorCode.CHALLENGE_INVALID);
|
|
556
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('should throw NAuthException if session expired', async () => {
|
|
561
|
+
const expiredSession = {
|
|
562
|
+
...mockChallengeSession,
|
|
563
|
+
expiresAt: new Date(Date.now() - 1000), // 1 second ago
|
|
564
|
+
};
|
|
565
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(expiredSession as any);
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
await service.validateSession('session-token-123');
|
|
569
|
+
fail('Should have thrown NAuthException');
|
|
570
|
+
} catch (error) {
|
|
571
|
+
expect(error).toBeInstanceOf(NAuthException);
|
|
572
|
+
expect((error as NAuthException).code).toBe(AuthErrorCode.CHALLENGE_EXPIRED);
|
|
573
|
+
expect((error as NAuthException).message).toContain('expired');
|
|
574
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('should throw NAuthException if session expires exactly at current time', async () => {
|
|
579
|
+
jest.useFakeTimers();
|
|
580
|
+
const now = new Date();
|
|
581
|
+
jest.setSystemTime(now);
|
|
582
|
+
|
|
583
|
+
const exactlyExpiredSession = {
|
|
584
|
+
...mockChallengeSession,
|
|
585
|
+
expiresAt: new Date(now.getTime() - 1), // 1ms ago
|
|
586
|
+
};
|
|
587
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(exactlyExpiredSession as any);
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
await service.validateSession('session-token-123');
|
|
591
|
+
fail('Should have thrown NAuthException');
|
|
592
|
+
} catch (error) {
|
|
593
|
+
expect(error).toBeInstanceOf(NAuthException);
|
|
594
|
+
expect((error as NAuthException).code).toBe(AuthErrorCode.CHALLENGE_EXPIRED);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
jest.useRealTimers();
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('should throw NAuthException if session already completed', async () => {
|
|
601
|
+
const completedSession = {
|
|
602
|
+
...mockChallengeSession,
|
|
603
|
+
isCompleted: true,
|
|
604
|
+
};
|
|
605
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(completedSession as any);
|
|
606
|
+
|
|
607
|
+
try {
|
|
608
|
+
await service.validateSession('session-token-123');
|
|
609
|
+
fail('Should have thrown NAuthException');
|
|
610
|
+
} catch (error) {
|
|
611
|
+
expect(error).toBeInstanceOf(NAuthException);
|
|
612
|
+
expect((error as NAuthException).code).toBe(AuthErrorCode.CHALLENGE_ALREADY_COMPLETED);
|
|
613
|
+
expect((error as NAuthException).message).toContain('already been completed');
|
|
614
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('should throw NAuthException if max attempts exceeded', async () => {
|
|
619
|
+
const maxAttemptsSession = {
|
|
620
|
+
...mockChallengeSession,
|
|
621
|
+
attempts: 3,
|
|
622
|
+
maxAttempts: 3,
|
|
623
|
+
};
|
|
624
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(maxAttemptsSession as any);
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
await service.validateSession('session-token-123');
|
|
628
|
+
fail('Should have thrown NAuthException');
|
|
629
|
+
} catch (error) {
|
|
630
|
+
expect(error).toBeInstanceOf(NAuthException);
|
|
631
|
+
expect((error as NAuthException).code).toBe(AuthErrorCode.CHALLENGE_MAX_ATTEMPTS);
|
|
632
|
+
expect((error as NAuthException).message).toContain('Maximum challenge attempts exceeded');
|
|
633
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('should throw NAuthException if attempts exceed max attempts', async () => {
|
|
638
|
+
const overMaxAttemptsSession = {
|
|
639
|
+
...mockChallengeSession,
|
|
640
|
+
attempts: 4,
|
|
641
|
+
maxAttempts: 3,
|
|
642
|
+
};
|
|
643
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(overMaxAttemptsSession as any);
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
await service.validateSession('session-token-123');
|
|
647
|
+
fail('Should have thrown NAuthException');
|
|
648
|
+
} catch (error) {
|
|
649
|
+
expect(error).toBeInstanceOf(NAuthException);
|
|
650
|
+
expect((error as NAuthException).code).toBe(AuthErrorCode.CHALLENGE_MAX_ATTEMPTS);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('should validate session when attempts are just below max', async () => {
|
|
655
|
+
const validSession = {
|
|
656
|
+
...mockChallengeSession,
|
|
657
|
+
expiresAt: new Date(Date.now() + 60000),
|
|
658
|
+
attempts: 2,
|
|
659
|
+
maxAttempts: 3,
|
|
660
|
+
};
|
|
661
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(validSession as any);
|
|
662
|
+
|
|
663
|
+
const result = await service.validateSession('session-token-123');
|
|
664
|
+
|
|
665
|
+
expect(result).toBeDefined();
|
|
666
|
+
expect(result.attempts).toBe(2);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('should throw NAuthException if challenge type mismatch', async () => {
|
|
670
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(mockChallengeSession as any);
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
await service.validateSession('session-token-123', AuthChallenge.VERIFY_PHONE);
|
|
674
|
+
fail('Should have thrown NAuthException');
|
|
675
|
+
} catch (error) {
|
|
676
|
+
expect(error).toBeInstanceOf(NAuthException);
|
|
677
|
+
expect((error as NAuthException).code).toBe(AuthErrorCode.CHALLENGE_TYPE_MISMATCH);
|
|
678
|
+
expect((error as NAuthException).message).toContain('Invalid challenge type');
|
|
679
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('should validate session without expected challenge type', async () => {
|
|
684
|
+
const validSession = {
|
|
685
|
+
...mockChallengeSession,
|
|
686
|
+
expiresAt: new Date(Date.now() + 60000),
|
|
687
|
+
};
|
|
688
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(validSession as any);
|
|
689
|
+
|
|
690
|
+
const result = await service.validateSession('session-token-123');
|
|
691
|
+
|
|
692
|
+
expect(result).toBeDefined();
|
|
693
|
+
expect(result.sessionToken).toBe('session-token-123');
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('should load session with user relation', async () => {
|
|
697
|
+
const validSession = {
|
|
698
|
+
...mockChallengeSession,
|
|
699
|
+
expiresAt: new Date(Date.now() + 60000),
|
|
700
|
+
};
|
|
701
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(validSession as any);
|
|
702
|
+
|
|
703
|
+
await service.validateSession('session-token-123');
|
|
704
|
+
|
|
705
|
+
// Verify findOne is called with relations to load user
|
|
706
|
+
expect(mockChallengeSessionRepository.findOne).toHaveBeenCalledWith({
|
|
707
|
+
where: { sessionToken: 'session-token-123' },
|
|
708
|
+
relations: ['user'],
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// ============================================================================
|
|
714
|
+
// incrementAttempts() Method
|
|
715
|
+
// ============================================================================
|
|
716
|
+
|
|
717
|
+
describe('incrementAttempts', () => {
|
|
718
|
+
it('should increment attempt counter', async () => {
|
|
719
|
+
const session = { ...mockChallengeSession, attempts: 1 } as IChallengeSession;
|
|
720
|
+
const updatedSession = { ...session, attempts: 2 };
|
|
721
|
+
mockChallengeSessionRepository.save.mockResolvedValue(updatedSession as any);
|
|
722
|
+
|
|
723
|
+
const result = await service.incrementAttempts(session);
|
|
724
|
+
|
|
725
|
+
expect(result.attempts).toBe(2);
|
|
726
|
+
expect(mockChallengeSessionRepository.save).toHaveBeenCalledWith(session);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it('should record audit event when max attempts exceeded', async () => {
|
|
730
|
+
const session = {
|
|
731
|
+
...mockChallengeSession,
|
|
732
|
+
attempts: 2,
|
|
733
|
+
maxAttempts: 3,
|
|
734
|
+
} as IChallengeSession;
|
|
735
|
+
const updatedSession = { ...session, attempts: 3 };
|
|
736
|
+
mockChallengeSessionRepository.save.mockResolvedValue(updatedSession as any);
|
|
737
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
738
|
+
|
|
739
|
+
await service.incrementAttempts(session);
|
|
740
|
+
|
|
741
|
+
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
742
|
+
(expect as any).objectContaining({
|
|
743
|
+
eventType: AuthAuditEventType.CHALLENGE_ATTEMPT_FAILED,
|
|
744
|
+
eventStatus: 'FAILURE',
|
|
745
|
+
reason: 'max_attempts_exceeded',
|
|
746
|
+
}),
|
|
747
|
+
);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('should not record audit event when max attempts not exceeded', async () => {
|
|
751
|
+
const session = {
|
|
752
|
+
...mockChallengeSession,
|
|
753
|
+
attempts: 1,
|
|
754
|
+
maxAttempts: 3,
|
|
755
|
+
} as IChallengeSession;
|
|
756
|
+
const updatedSession = { ...session, attempts: 2 };
|
|
757
|
+
mockChallengeSessionRepository.save.mockResolvedValue(updatedSession as any);
|
|
758
|
+
|
|
759
|
+
await service.incrementAttempts(session);
|
|
760
|
+
|
|
761
|
+
expect(mockAuditService.recordEvent).not.toHaveBeenCalled();
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it('should handle audit service errors gracefully', async () => {
|
|
765
|
+
const session = {
|
|
766
|
+
...mockChallengeSession,
|
|
767
|
+
attempts: 2,
|
|
768
|
+
maxAttempts: 3,
|
|
769
|
+
} as IChallengeSession;
|
|
770
|
+
const updatedSession = { ...session, attempts: 3 };
|
|
771
|
+
mockChallengeSessionRepository.save.mockResolvedValue(updatedSession as any);
|
|
772
|
+
mockAuditService.recordEvent.mockRejectedValue(new Error('Audit error'));
|
|
773
|
+
|
|
774
|
+
const result = await service.incrementAttempts(session);
|
|
775
|
+
|
|
776
|
+
expect(result.attempts).toBe(3);
|
|
777
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('should handle non-Error audit exceptions in incrementAttempts', async () => {
|
|
781
|
+
const session = {
|
|
782
|
+
...mockChallengeSession,
|
|
783
|
+
attempts: 2,
|
|
784
|
+
maxAttempts: 3,
|
|
785
|
+
} as IChallengeSession;
|
|
786
|
+
const updatedSession = { ...session, attempts: 3 };
|
|
787
|
+
mockChallengeSessionRepository.save.mockResolvedValue(updatedSession as any);
|
|
788
|
+
mockAuditService.recordEvent.mockRejectedValue('String error' as any);
|
|
789
|
+
|
|
790
|
+
const result = await service.incrementAttempts(session);
|
|
791
|
+
|
|
792
|
+
expect(result.attempts).toBe(3);
|
|
793
|
+
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
794
|
+
(expect as any).stringContaining('Failed to record CHALLENGE_ATTEMPT_FAILED audit event: Unknown error'),
|
|
795
|
+
(expect as any).any(Object),
|
|
796
|
+
);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it('should record audit event with session IP and userAgent when available', async () => {
|
|
800
|
+
const session = {
|
|
801
|
+
...mockChallengeSession,
|
|
802
|
+
attempts: 2,
|
|
803
|
+
maxAttempts: 3,
|
|
804
|
+
ipAddress: '10.20.30.40',
|
|
805
|
+
userAgent: 'custom-browser-agent',
|
|
806
|
+
} as IChallengeSession;
|
|
807
|
+
const updatedSession = { ...session, attempts: 3 };
|
|
808
|
+
mockChallengeSessionRepository.save.mockResolvedValue(updatedSession as any);
|
|
809
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
810
|
+
|
|
811
|
+
await service.incrementAttempts(session);
|
|
812
|
+
|
|
813
|
+
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
814
|
+
(expect as any).objectContaining({
|
|
815
|
+
ipAddress: '10.20.30.40',
|
|
816
|
+
userAgent: 'custom-browser-agent',
|
|
817
|
+
}),
|
|
818
|
+
);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('should record audit event with undefined IP/userAgent when session values are null', async () => {
|
|
822
|
+
const session = {
|
|
823
|
+
...mockChallengeSession,
|
|
824
|
+
attempts: 2,
|
|
825
|
+
maxAttempts: 3,
|
|
826
|
+
ipAddress: null,
|
|
827
|
+
userAgent: null,
|
|
828
|
+
} as IChallengeSession;
|
|
829
|
+
const updatedSession = { ...session, attempts: 3 };
|
|
830
|
+
mockChallengeSessionRepository.save.mockResolvedValue(updatedSession as any);
|
|
831
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
832
|
+
|
|
833
|
+
await service.incrementAttempts(session);
|
|
834
|
+
|
|
835
|
+
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
836
|
+
(expect as any).objectContaining({
|
|
837
|
+
ipAddress: undefined,
|
|
838
|
+
userAgent: undefined,
|
|
839
|
+
}),
|
|
840
|
+
);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('should handle audit logging when incrementAttempts reaches exactly max attempts', async () => {
|
|
844
|
+
const session = {
|
|
845
|
+
...mockChallengeSession,
|
|
846
|
+
attempts: 2,
|
|
847
|
+
maxAttempts: 3,
|
|
848
|
+
} as IChallengeSession;
|
|
849
|
+
const updatedSession = { ...session, attempts: 3 };
|
|
850
|
+
mockChallengeSessionRepository.save.mockResolvedValue(updatedSession as any);
|
|
851
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
852
|
+
|
|
853
|
+
await service.incrementAttempts(session);
|
|
854
|
+
|
|
855
|
+
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
856
|
+
(expect as any).objectContaining({
|
|
857
|
+
eventType: AuthAuditEventType.CHALLENGE_ATTEMPT_FAILED,
|
|
858
|
+
description: (expect as any).stringContaining('maximum attempts (3) exceeded'),
|
|
859
|
+
}),
|
|
860
|
+
);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it('should handle session without user gracefully', async () => {
|
|
864
|
+
const sessionWithoutUser = {
|
|
865
|
+
...mockChallengeSession,
|
|
866
|
+
attempts: 2,
|
|
867
|
+
maxAttempts: 3,
|
|
868
|
+
user: undefined,
|
|
869
|
+
} as any;
|
|
870
|
+
const updatedSession = { ...sessionWithoutUser, attempts: 3 };
|
|
871
|
+
mockChallengeSessionRepository.save.mockResolvedValue(updatedSession);
|
|
872
|
+
|
|
873
|
+
await service.incrementAttempts(sessionWithoutUser);
|
|
874
|
+
|
|
875
|
+
// Should not throw, but audit may not be recorded
|
|
876
|
+
expect(mockChallengeSessionRepository.save).toHaveBeenCalled();
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// ============================================================================
|
|
881
|
+
// validateAndConsumeSession() Method
|
|
882
|
+
// ============================================================================
|
|
883
|
+
|
|
884
|
+
describe('validateAndConsumeSession', () => {
|
|
885
|
+
it('should validate and mark session as completed', async () => {
|
|
886
|
+
const validSession = {
|
|
887
|
+
...mockChallengeSession,
|
|
888
|
+
expiresAt: new Date(Date.now() + 60000),
|
|
889
|
+
};
|
|
890
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(validSession as any);
|
|
891
|
+
const completedSession = { ...validSession, isCompleted: true, completedAt: new Date() };
|
|
892
|
+
mockChallengeSessionRepository.save.mockResolvedValue(completedSession as any);
|
|
893
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
894
|
+
|
|
895
|
+
const result = await service.validateAndConsumeSession('session-token-123', AuthChallenge.VERIFY_EMAIL);
|
|
896
|
+
|
|
897
|
+
expect(result.isCompleted).toBe(true);
|
|
898
|
+
expect(result.completedAt).toBeDefined();
|
|
899
|
+
expect(mockChallengeSessionRepository.save).toHaveBeenCalled();
|
|
900
|
+
expect(mockLogger.log).toHaveBeenCalled();
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it('should record audit event on session completion', async () => {
|
|
904
|
+
const validSession = {
|
|
905
|
+
...mockChallengeSession,
|
|
906
|
+
expiresAt: new Date(Date.now() + 60000),
|
|
907
|
+
};
|
|
908
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(validSession as any);
|
|
909
|
+
const completedSession = { ...validSession, isCompleted: true };
|
|
910
|
+
mockChallengeSessionRepository.save.mockResolvedValue(completedSession as any);
|
|
911
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
912
|
+
|
|
913
|
+
await service.validateAndConsumeSession('session-token-123', AuthChallenge.VERIFY_EMAIL);
|
|
914
|
+
|
|
915
|
+
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
916
|
+
(expect as any).objectContaining({
|
|
917
|
+
eventType: AuthAuditEventType.CHALLENGE_COMPLETED,
|
|
918
|
+
eventStatus: 'SUCCESS',
|
|
919
|
+
userId: mockUser.id,
|
|
920
|
+
}),
|
|
921
|
+
);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
it('should handle audit service errors gracefully', async () => {
|
|
925
|
+
const validSession = {
|
|
926
|
+
...mockChallengeSession,
|
|
927
|
+
expiresAt: new Date(Date.now() + 60000),
|
|
928
|
+
};
|
|
929
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(validSession as any);
|
|
930
|
+
const completedSession = { ...validSession, isCompleted: true };
|
|
931
|
+
mockChallengeSessionRepository.save.mockResolvedValue(completedSession as any);
|
|
932
|
+
mockAuditService.recordEvent.mockRejectedValue(new Error('Audit error'));
|
|
933
|
+
|
|
934
|
+
const result = await service.validateAndConsumeSession('session-token-123', AuthChallenge.VERIFY_EMAIL);
|
|
935
|
+
|
|
936
|
+
expect(result.isCompleted).toBe(true);
|
|
937
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('should handle non-Error audit exceptions in validateAndConsumeSession', async () => {
|
|
941
|
+
const validSession = {
|
|
942
|
+
...mockChallengeSession,
|
|
943
|
+
expiresAt: new Date(Date.now() + 60000),
|
|
944
|
+
};
|
|
945
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(validSession as any);
|
|
946
|
+
const completedSession = { ...validSession, isCompleted: true };
|
|
947
|
+
mockChallengeSessionRepository.save.mockResolvedValue(completedSession as any);
|
|
948
|
+
mockAuditService.recordEvent.mockRejectedValue('String error' as any);
|
|
949
|
+
|
|
950
|
+
const result = await service.validateAndConsumeSession('session-token-123', AuthChallenge.VERIFY_EMAIL);
|
|
951
|
+
|
|
952
|
+
expect(result.isCompleted).toBe(true);
|
|
953
|
+
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
954
|
+
(expect as any).stringContaining('Failed to record CHALLENGE_COMPLETED audit event: Unknown error'),
|
|
955
|
+
(expect as any).any(Object),
|
|
956
|
+
);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it('should handle validateAndConsumeSession with null session IP and userAgent', async () => {
|
|
960
|
+
const validSession = {
|
|
961
|
+
...mockChallengeSession,
|
|
962
|
+
expiresAt: new Date(Date.now() + 60000),
|
|
963
|
+
ipAddress: null,
|
|
964
|
+
userAgent: null,
|
|
965
|
+
};
|
|
966
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(validSession as any);
|
|
967
|
+
const completedSession = { ...validSession, isCompleted: true };
|
|
968
|
+
mockChallengeSessionRepository.save.mockResolvedValue(completedSession as any);
|
|
969
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
970
|
+
|
|
971
|
+
await service.validateAndConsumeSession('session-token-123', AuthChallenge.VERIFY_EMAIL);
|
|
972
|
+
|
|
973
|
+
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
974
|
+
(expect as any).objectContaining({
|
|
975
|
+
ipAddress: undefined,
|
|
976
|
+
userAgent: undefined,
|
|
977
|
+
}),
|
|
978
|
+
);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
it('should throw if validation fails', async () => {
|
|
982
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(null);
|
|
983
|
+
|
|
984
|
+
try {
|
|
985
|
+
await service.validateAndConsumeSession('invalid-token', AuthChallenge.VERIFY_EMAIL);
|
|
986
|
+
fail('Should have thrown NAuthException');
|
|
987
|
+
} catch (error) {
|
|
988
|
+
expect(error).toBeInstanceOf(NAuthException);
|
|
989
|
+
expect(mockChallengeSessionRepository.save).not.toHaveBeenCalled();
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
it('should use session IP and user agent in audit event', async () => {
|
|
994
|
+
const validSession = {
|
|
995
|
+
...mockChallengeSession,
|
|
996
|
+
expiresAt: new Date(Date.now() + 60000),
|
|
997
|
+
ipAddress: '5.6.7.8',
|
|
998
|
+
userAgent: 'session-agent',
|
|
999
|
+
};
|
|
1000
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(validSession as any);
|
|
1001
|
+
const completedSession = { ...validSession, isCompleted: true };
|
|
1002
|
+
mockChallengeSessionRepository.save.mockResolvedValue(completedSession as any);
|
|
1003
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
1004
|
+
|
|
1005
|
+
await service.validateAndConsumeSession('session-token-123', AuthChallenge.VERIFY_EMAIL);
|
|
1006
|
+
|
|
1007
|
+
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
1008
|
+
(expect as any).objectContaining({
|
|
1009
|
+
ipAddress: '5.6.7.8',
|
|
1010
|
+
userAgent: 'session-agent',
|
|
1011
|
+
}),
|
|
1012
|
+
);
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// ============================================================================
|
|
1017
|
+
// cleanupExpiredSessions() Method
|
|
1018
|
+
// ============================================================================
|
|
1019
|
+
|
|
1020
|
+
describe('cleanupExpiredSessions', () => {
|
|
1021
|
+
it('should delete expired and completed sessions', async () => {
|
|
1022
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 5 } as any);
|
|
1023
|
+
|
|
1024
|
+
await service.cleanupExpiredSessions(1);
|
|
1025
|
+
|
|
1026
|
+
expect(mockChallengeSessionRepository.delete).toHaveBeenCalledTimes(2); // Once for expired, once for completed
|
|
1027
|
+
expect(mockChallengeSessionRepository.delete).toHaveBeenCalledWith(
|
|
1028
|
+
(expect as any).objectContaining({
|
|
1029
|
+
userId: 1,
|
|
1030
|
+
}),
|
|
1031
|
+
);
|
|
1032
|
+
expect(mockChallengeSessionRepository.delete).toHaveBeenCalledWith(
|
|
1033
|
+
(expect as any).objectContaining({
|
|
1034
|
+
userId: 1,
|
|
1035
|
+
isCompleted: true,
|
|
1036
|
+
}),
|
|
1037
|
+
);
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
it('should handle cleanup with no sessions to delete', async () => {
|
|
1041
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
1042
|
+
|
|
1043
|
+
await service.cleanupExpiredSessions(1);
|
|
1044
|
+
|
|
1045
|
+
expect(mockChallengeSessionRepository.delete).toHaveBeenCalledTimes(2);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
it('should use LessThan for expiration check', async () => {
|
|
1049
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
1050
|
+
|
|
1051
|
+
await service.cleanupExpiredSessions(1);
|
|
1052
|
+
|
|
1053
|
+
// LessThan is a TypeORM operator - we can't easily test it, but we verify the call was made
|
|
1054
|
+
expect(mockChallengeSessionRepository.delete).toHaveBeenCalled();
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
// ============================================================================
|
|
1059
|
+
// cleanupAllExpiredSessions() Method
|
|
1060
|
+
// ============================================================================
|
|
1061
|
+
|
|
1062
|
+
describe('cleanupAllExpiredSessions', () => {
|
|
1063
|
+
it('should delete all expired sessions and return count', async () => {
|
|
1064
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 10 } as any);
|
|
1065
|
+
|
|
1066
|
+
const result = await service.cleanupAllExpiredSessions();
|
|
1067
|
+
|
|
1068
|
+
expect(result).toBe(10);
|
|
1069
|
+
expect(mockChallengeSessionRepository.delete).toHaveBeenCalledWith((expect as any).objectContaining({}));
|
|
1070
|
+
expect(mockLogger.log).toHaveBeenCalledWith(
|
|
1071
|
+
(expect as any).stringContaining('Cleaned up 10 expired challenge sessions'),
|
|
1072
|
+
);
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
it('should return 0 when no sessions deleted', async () => {
|
|
1076
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
1077
|
+
|
|
1078
|
+
const result = await service.cleanupAllExpiredSessions();
|
|
1079
|
+
|
|
1080
|
+
expect(result).toBe(0);
|
|
1081
|
+
expect(mockLogger.log).toHaveBeenCalledWith(
|
|
1082
|
+
(expect as any).stringContaining('Cleaned up 0 expired challenge sessions'),
|
|
1083
|
+
);
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
it('should handle delete result without affected property', async () => {
|
|
1087
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({} as any);
|
|
1088
|
+
|
|
1089
|
+
const result = await service.cleanupAllExpiredSessions();
|
|
1090
|
+
|
|
1091
|
+
expect(result).toBe(0);
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
// ============================================================================
|
|
1096
|
+
// deleteUserChallengeSessions() Method
|
|
1097
|
+
// ============================================================================
|
|
1098
|
+
|
|
1099
|
+
describe('deleteUserChallengeSessions', () => {
|
|
1100
|
+
it('should delete active challenge sessions by type', async () => {
|
|
1101
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 2 } as any);
|
|
1102
|
+
|
|
1103
|
+
const result = await service.deleteUserChallengeSessions(1, AuthChallenge.MFA_SETUP_REQUIRED);
|
|
1104
|
+
|
|
1105
|
+
expect(result).toBe(2);
|
|
1106
|
+
expect(mockChallengeSessionRepository.delete).toHaveBeenCalledWith({
|
|
1107
|
+
userId: 1,
|
|
1108
|
+
challengeName: AuthChallenge.MFA_SETUP_REQUIRED,
|
|
1109
|
+
isCompleted: false,
|
|
1110
|
+
});
|
|
1111
|
+
expect(mockLogger.log).toHaveBeenCalledWith(
|
|
1112
|
+
(expect as any).stringContaining('Deleted 2 MFA_SETUP_REQUIRED challenge session(s)'),
|
|
1113
|
+
);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
it('should return 0 when no sessions deleted', async () => {
|
|
1117
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
1118
|
+
|
|
1119
|
+
const result = await service.deleteUserChallengeSessions(1, AuthChallenge.VERIFY_EMAIL);
|
|
1120
|
+
|
|
1121
|
+
expect(result).toBe(0);
|
|
1122
|
+
expect(mockLogger.log).not.toHaveBeenCalled();
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
it('should delete sessions for all challenge types', async () => {
|
|
1126
|
+
const challengeTypes = [
|
|
1127
|
+
AuthChallenge.VERIFY_EMAIL,
|
|
1128
|
+
AuthChallenge.VERIFY_PHONE,
|
|
1129
|
+
AuthChallenge.MFA_REQUIRED,
|
|
1130
|
+
AuthChallenge.MFA_SETUP_REQUIRED,
|
|
1131
|
+
AuthChallenge.FORCE_CHANGE_PASSWORD,
|
|
1132
|
+
];
|
|
1133
|
+
|
|
1134
|
+
for (const challengeType of challengeTypes) {
|
|
1135
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 1 } as any);
|
|
1136
|
+
const result = await service.deleteUserChallengeSessions(1, challengeType);
|
|
1137
|
+
expect(result).toBe(1);
|
|
1138
|
+
expect(mockChallengeSessionRepository.delete).toHaveBeenCalledWith(
|
|
1139
|
+
(expect as any).objectContaining({
|
|
1140
|
+
challengeName: challengeType,
|
|
1141
|
+
}),
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
// ============================================================================
|
|
1148
|
+
// Helper Methods
|
|
1149
|
+
// ============================================================================
|
|
1150
|
+
|
|
1151
|
+
describe('maskEmail', () => {
|
|
1152
|
+
it('should mask email address correctly', () => {
|
|
1153
|
+
const masked = service.maskEmail('john.doe@example.com');
|
|
1154
|
+
expect(masked).toBe('j***@example.com');
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
it('should handle single character local part', () => {
|
|
1158
|
+
const masked = service.maskEmail('j@example.com');
|
|
1159
|
+
expect(masked).toBe('j***@example.com');
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
it('should handle invalid email gracefully', () => {
|
|
1163
|
+
const masked = service.maskEmail('invalid-email');
|
|
1164
|
+
expect(masked).toBe('invalid-email');
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
it('should handle email without @ symbol', () => {
|
|
1168
|
+
const masked = service.maskEmail('noatdomain');
|
|
1169
|
+
expect(masked).toBe('noatdomain');
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
it('should handle empty email', () => {
|
|
1173
|
+
const masked = service.maskEmail('');
|
|
1174
|
+
expect(masked).toBe('');
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
it('should mask long email addresses', () => {
|
|
1178
|
+
const masked = service.maskEmail('verylongemailaddress@example.com');
|
|
1179
|
+
expect(masked).toBe('v***@example.com');
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
it('should handle email with empty local part', () => {
|
|
1183
|
+
const masked = service.maskEmail('@example.com');
|
|
1184
|
+
// When split('@'), first part is empty string, so localPart[0] is undefined
|
|
1185
|
+
// Implementation concatenates undefined with '***@' resulting in 'undefined***@example.com'
|
|
1186
|
+
expect(masked).toBe('undefined***@example.com');
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
it('should handle email with multiple @ symbols', () => {
|
|
1190
|
+
// split('@') on 'invalid@email@example.com' creates ['invalid', 'email', 'example.com']
|
|
1191
|
+
// Takes first element as localPart and second as domain
|
|
1192
|
+
const masked = service.maskEmail('invalid@email@example.com');
|
|
1193
|
+
expect(masked).toBe('i***@email');
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
it('should handle email with special characters in local part', () => {
|
|
1197
|
+
const masked = service.maskEmail('user+tag@example.com');
|
|
1198
|
+
expect(masked).toBe('u***@example.com');
|
|
1199
|
+
});
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
describe('maskPhone', () => {
|
|
1203
|
+
it('should mask phone number correctly', () => {
|
|
1204
|
+
const masked = service.maskPhone('+1234567890');
|
|
1205
|
+
expect(masked).toBe('***-***-7890');
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
it('should handle phone with formatting', () => {
|
|
1209
|
+
const masked = service.maskPhone('+1 (234) 567-8901');
|
|
1210
|
+
expect(masked).toBe('***-***-8901');
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
it('should handle short phone numbers', () => {
|
|
1214
|
+
const masked = service.maskPhone('123');
|
|
1215
|
+
expect(masked).toBe('123');
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
it('should handle phone with exactly 4 digits', () => {
|
|
1219
|
+
const masked = service.maskPhone('1234');
|
|
1220
|
+
expect(masked).toBe('***-***-1234');
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
it('should handle phone with less than 4 digits', () => {
|
|
1224
|
+
const masked = service.maskPhone('12');
|
|
1225
|
+
expect(masked).toBe('12');
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
it('should handle phone with only special characters', () => {
|
|
1229
|
+
const masked = service.maskPhone('+--()');
|
|
1230
|
+
expect(masked).toBe('+--()');
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
it('should handle international phone numbers', () => {
|
|
1234
|
+
const masked = service.maskPhone('+441234567890');
|
|
1235
|
+
expect(masked).toBe('***-***-7890');
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
it('should handle empty phone', () => {
|
|
1239
|
+
const masked = service.maskPhone('');
|
|
1240
|
+
expect(masked).toBe('');
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
it('should handle phone with only letters', () => {
|
|
1244
|
+
const masked = service.maskPhone('abc');
|
|
1245
|
+
expect(masked).toBe('abc');
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
it('should handle phone with mixed characters', () => {
|
|
1249
|
+
// maskPhone extracts digits only: '+1-800-CALL-NOW' -> '1800' -> last 4 = '1800'
|
|
1250
|
+
const masked = service.maskPhone('+1-800-CALL-NOW');
|
|
1251
|
+
expect(masked).toBe('***-***-1800');
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
it('should handle phone with exactly 3 digits', () => {
|
|
1255
|
+
const masked = service.maskPhone('123');
|
|
1256
|
+
expect(masked).toBe('123');
|
|
1257
|
+
});
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
// ============================================================================
|
|
1261
|
+
// Edge Cases and Error Handling
|
|
1262
|
+
// ============================================================================
|
|
1263
|
+
|
|
1264
|
+
describe('Edge Cases', () => {
|
|
1265
|
+
it('should handle service without audit service', async () => {
|
|
1266
|
+
const serviceWithoutAudit = new ChallengeService(
|
|
1267
|
+
mockChallengeSessionRepository,
|
|
1268
|
+
mockClientInfoService,
|
|
1269
|
+
mockLogger,
|
|
1270
|
+
undefined, // No audit service
|
|
1271
|
+
);
|
|
1272
|
+
|
|
1273
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
1274
|
+
mockChallengeSessionRepository.create.mockReturnValue(mockChallengeSession as any);
|
|
1275
|
+
mockChallengeSessionRepository.save.mockResolvedValue(mockChallengeSession as any);
|
|
1276
|
+
|
|
1277
|
+
const result = await serviceWithoutAudit.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL);
|
|
1278
|
+
|
|
1279
|
+
expect(result).toBeDefined();
|
|
1280
|
+
// Should not throw
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
it('should handle repository save errors', async () => {
|
|
1284
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
1285
|
+
mockChallengeSessionRepository.create.mockReturnValue(mockChallengeSession as any);
|
|
1286
|
+
mockChallengeSessionRepository.save.mockRejectedValue(new Error('Database error'));
|
|
1287
|
+
|
|
1288
|
+
try {
|
|
1289
|
+
await service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL);
|
|
1290
|
+
fail('Should have thrown error');
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
expect((error as Error).message).toContain('Database error');
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
it('should handle concurrent session creation', async () => {
|
|
1297
|
+
mockChallengeSessionRepository.delete.mockResolvedValue({ affected: 0 } as any);
|
|
1298
|
+
mockChallengeSessionRepository.create
|
|
1299
|
+
.mockReturnValueOnce({ ...mockChallengeSession, challengeName: AuthChallenge.VERIFY_EMAIL } as any)
|
|
1300
|
+
.mockReturnValueOnce({ ...mockChallengeSession, challengeName: AuthChallenge.VERIFY_PHONE } as any);
|
|
1301
|
+
mockChallengeSessionRepository.save
|
|
1302
|
+
.mockResolvedValueOnce({ ...mockChallengeSession, challengeName: AuthChallenge.VERIFY_EMAIL } as any)
|
|
1303
|
+
.mockResolvedValueOnce({ ...mockChallengeSession, challengeName: AuthChallenge.VERIFY_PHONE } as any);
|
|
1304
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
1305
|
+
|
|
1306
|
+
const promises = [
|
|
1307
|
+
service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_EMAIL),
|
|
1308
|
+
service.createChallengeSession(mockUser as IUser, AuthChallenge.VERIFY_PHONE),
|
|
1309
|
+
];
|
|
1310
|
+
|
|
1311
|
+
const results = await Promise.all(promises);
|
|
1312
|
+
|
|
1313
|
+
expect(results.length).toBe(2);
|
|
1314
|
+
expect(results[0].challengeName).toBe(AuthChallenge.VERIFY_EMAIL);
|
|
1315
|
+
expect(results[1].challengeName).toBe(AuthChallenge.VERIFY_PHONE);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
it('should handle validateAndConsumeSession for all challenge types', async () => {
|
|
1319
|
+
const challengeTypes = [
|
|
1320
|
+
AuthChallenge.VERIFY_EMAIL,
|
|
1321
|
+
AuthChallenge.VERIFY_PHONE,
|
|
1322
|
+
AuthChallenge.MFA_REQUIRED,
|
|
1323
|
+
AuthChallenge.MFA_SETUP_REQUIRED,
|
|
1324
|
+
AuthChallenge.FORCE_CHANGE_PASSWORD,
|
|
1325
|
+
];
|
|
1326
|
+
|
|
1327
|
+
for (const challengeType of challengeTypes) {
|
|
1328
|
+
const validSession = {
|
|
1329
|
+
...mockChallengeSession,
|
|
1330
|
+
challengeName: challengeType,
|
|
1331
|
+
expiresAt: new Date(Date.now() + 60000),
|
|
1332
|
+
};
|
|
1333
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(validSession as any);
|
|
1334
|
+
const completedSession = { ...validSession, isCompleted: true };
|
|
1335
|
+
mockChallengeSessionRepository.save.mockResolvedValue(completedSession as any);
|
|
1336
|
+
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
1337
|
+
|
|
1338
|
+
const result = await service.validateAndConsumeSession('session-token-123', challengeType);
|
|
1339
|
+
|
|
1340
|
+
expect(result.isCompleted).toBe(true);
|
|
1341
|
+
expect(result.challengeName).toBe(challengeType);
|
|
1342
|
+
jest.clearAllMocks();
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
it('should handle session validation with null user gracefully', async () => {
|
|
1347
|
+
const sessionWithoutUser = {
|
|
1348
|
+
...mockChallengeSession,
|
|
1349
|
+
expiresAt: new Date(Date.now() + 60000),
|
|
1350
|
+
user: null,
|
|
1351
|
+
};
|
|
1352
|
+
mockChallengeSessionRepository.findOne.mockResolvedValue(sessionWithoutUser as any);
|
|
1353
|
+
|
|
1354
|
+
try {
|
|
1355
|
+
await service.validateSession('session-token-123');
|
|
1356
|
+
// Should not throw if user is null but session is valid
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
// Only expiresAt check might fail if user is needed for logging
|
|
1359
|
+
// But validation should still work
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
});
|
|
1363
|
+
});
|