@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,1292 @@
|
|
|
1
|
+
import { Repository } from 'typeorm';
|
|
2
|
+
import { RiskDetectionService } from './risk-detection.service';
|
|
3
|
+
import { IUser, ISession } from '../interfaces/entities.interface';
|
|
4
|
+
import { ClientInfo } from '../interfaces/client-info.interface';
|
|
5
|
+
import { NAuthConfig } from '../interfaces/config.interface';
|
|
6
|
+
import { NAuthLogger } from '../utils/nauth-logger';
|
|
7
|
+
import { RiskFactor } from '../enums/risk-factor.enum';
|
|
8
|
+
import { BaseSession, BaseAuthAudit } from '../entities';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Risk Detection Service Unit Tests
|
|
12
|
+
*
|
|
13
|
+
* Tests risk factor detection, double-counting prevention, error handling,
|
|
14
|
+
* and configuration-based trigger enabling/disabling.
|
|
15
|
+
*
|
|
16
|
+
* Platform-agnostic: Uses direct instantiation, no NestJS dependencies.
|
|
17
|
+
*/
|
|
18
|
+
describe('RiskDetectionService', () => {
|
|
19
|
+
let service: RiskDetectionService;
|
|
20
|
+
let mockSessionRepository: jest.Mocked<Repository<BaseSession>>;
|
|
21
|
+
let mockAuditRepository: jest.Mocked<Repository<BaseAuthAudit>>;
|
|
22
|
+
let mockTrustedDeviceService: jest.Mocked<any>;
|
|
23
|
+
let mockConfig: NAuthConfig;
|
|
24
|
+
let mockLogger: jest.Mocked<NAuthLogger>;
|
|
25
|
+
|
|
26
|
+
const mockUser: IUser = {
|
|
27
|
+
id: 1,
|
|
28
|
+
sub: 'user-123',
|
|
29
|
+
email: 'test@example.com',
|
|
30
|
+
username: 'testuser',
|
|
31
|
+
phone: null,
|
|
32
|
+
firstName: null,
|
|
33
|
+
lastName: null,
|
|
34
|
+
passwordHash: null,
|
|
35
|
+
passwordChangedAt: null,
|
|
36
|
+
passwordHistory: null,
|
|
37
|
+
isEmailVerified: true,
|
|
38
|
+
isPhoneVerified: false,
|
|
39
|
+
isActive: true,
|
|
40
|
+
mustChangePassword: false,
|
|
41
|
+
isLocked: false,
|
|
42
|
+
lockReason: null,
|
|
43
|
+
lockedAt: null,
|
|
44
|
+
lockedUntil: null,
|
|
45
|
+
failedLoginAttempts: 0,
|
|
46
|
+
lastFailedLoginAt: null,
|
|
47
|
+
lastLoginAt: null,
|
|
48
|
+
lastLoginIp: null,
|
|
49
|
+
hasSocialAuth: false,
|
|
50
|
+
socialProviders: null,
|
|
51
|
+
mfaEnabled: false,
|
|
52
|
+
mfaMethods: null,
|
|
53
|
+
preferredMfaMethod: null,
|
|
54
|
+
backupCodes: null,
|
|
55
|
+
metadata: null,
|
|
56
|
+
createdAt: new Date(),
|
|
57
|
+
updatedAt: new Date(),
|
|
58
|
+
deletedAt: null,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const mockClientInfo: ClientInfo = {
|
|
62
|
+
ipAddress: '192.168.1.100',
|
|
63
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
64
|
+
deviceToken: 'device-123',
|
|
65
|
+
deviceName: 'Chrome on Windows',
|
|
66
|
+
deviceType: 'desktop',
|
|
67
|
+
ipCountry: 'US',
|
|
68
|
+
ipCity: 'New York',
|
|
69
|
+
platform: 'Windows',
|
|
70
|
+
browser: 'Chrome',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
mockSessionRepository = {
|
|
75
|
+
findOne: jest.fn(),
|
|
76
|
+
find: jest.fn(),
|
|
77
|
+
} as any;
|
|
78
|
+
|
|
79
|
+
mockAuditRepository = {
|
|
80
|
+
findOne: jest.fn(),
|
|
81
|
+
find: jest.fn(),
|
|
82
|
+
} as any;
|
|
83
|
+
|
|
84
|
+
mockTrustedDeviceService = {
|
|
85
|
+
isDeviceTrusted: jest.fn(),
|
|
86
|
+
} as any;
|
|
87
|
+
|
|
88
|
+
mockLogger = {
|
|
89
|
+
log: jest.fn(),
|
|
90
|
+
error: jest.fn(),
|
|
91
|
+
warn: jest.fn(),
|
|
92
|
+
debug: jest.fn(),
|
|
93
|
+
verbose: jest.fn(),
|
|
94
|
+
} as any;
|
|
95
|
+
|
|
96
|
+
mockConfig = {
|
|
97
|
+
jwt: {
|
|
98
|
+
accessToken: { secret: 'test-secret', expiresIn: '15m' },
|
|
99
|
+
refreshToken: { secret: 'test-refresh-secret', expiresIn: '7d' },
|
|
100
|
+
},
|
|
101
|
+
mfa: {
|
|
102
|
+
adaptive: {
|
|
103
|
+
triggers: [
|
|
104
|
+
RiskFactor.NEW_DEVICE,
|
|
105
|
+
RiskFactor.NEW_IP,
|
|
106
|
+
RiskFactor.NEW_COUNTRY,
|
|
107
|
+
RiskFactor.IMPOSSIBLE_TRAVEL,
|
|
108
|
+
RiskFactor.SUSPICIOUS_ACTIVITY,
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Instantiate service directly
|
|
115
|
+
service = new RiskDetectionService(
|
|
116
|
+
mockSessionRepository,
|
|
117
|
+
mockAuditRepository,
|
|
118
|
+
mockConfig,
|
|
119
|
+
mockLogger,
|
|
120
|
+
mockTrustedDeviceService,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
afterEach(() => {
|
|
125
|
+
jest.clearAllMocks();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// Service Initialization
|
|
130
|
+
// ============================================================================
|
|
131
|
+
|
|
132
|
+
it('should be defined', () => {
|
|
133
|
+
expect(service).toBeDefined();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// detectRiskFactors() - NEW_DEVICE
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
describe('detectRiskFactors() - new_device', () => {
|
|
141
|
+
it('should detect new device when device never seen before', async () => {
|
|
142
|
+
// Setup: new_device check finds no session -> device is new
|
|
143
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // Device not found
|
|
144
|
+
// new_country check: country exists (optimized - 1 query, returns early)
|
|
145
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Country exists
|
|
146
|
+
// impossible_travel check: no previous session
|
|
147
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null);
|
|
148
|
+
// new_ip check: IP exists (since country exists, new_ip is still checked)
|
|
149
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // IP exists
|
|
150
|
+
// suspicious_activity check: no suspicious activity
|
|
151
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
152
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
153
|
+
|
|
154
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
155
|
+
|
|
156
|
+
expect(factors).toContain(RiskFactor.NEW_DEVICE);
|
|
157
|
+
expect(factors.length).toBe(1); // Only new_device, others are not new
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should not detect new device when device seen before', async () => {
|
|
161
|
+
mockSessionRepository.findOne
|
|
162
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
163
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
164
|
+
.mockResolvedValueOnce(null); // No previous session for impossible_travel
|
|
165
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
166
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
167
|
+
|
|
168
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
169
|
+
|
|
170
|
+
expect(factors).not.toContain(RiskFactor.NEW_DEVICE);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should skip new_device check if trigger not enabled', async () => {
|
|
174
|
+
mockConfig.mfa!.adaptive!.triggers = [RiskFactor.NEW_IP, RiskFactor.NEW_COUNTRY];
|
|
175
|
+
service = new RiskDetectionService(
|
|
176
|
+
mockSessionRepository,
|
|
177
|
+
mockAuditRepository,
|
|
178
|
+
mockConfig,
|
|
179
|
+
mockLogger,
|
|
180
|
+
mockTrustedDeviceService,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Country exists
|
|
184
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // No previous session
|
|
185
|
+
|
|
186
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
187
|
+
|
|
188
|
+
expect(factors).not.toContain(RiskFactor.NEW_DEVICE);
|
|
189
|
+
// Should not call findOne for device check
|
|
190
|
+
expect(mockSessionRepository.findOne).not.toHaveBeenCalledWith(
|
|
191
|
+
(expect as any).objectContaining({
|
|
192
|
+
where: (expect as any).objectContaining({ deviceId: mockClientInfo.deviceToken }),
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should skip new_device check if deviceToken not provided', async () => {
|
|
198
|
+
const clientInfoWithoutDevice = { ...mockClientInfo, deviceToken: undefined };
|
|
199
|
+
|
|
200
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Country exists
|
|
201
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // No previous session
|
|
202
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
203
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
204
|
+
|
|
205
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoWithoutDevice);
|
|
206
|
+
|
|
207
|
+
expect(factors).not.toContain(RiskFactor.NEW_DEVICE);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should not detect new device when device is trusted', async () => {
|
|
211
|
+
mockTrustedDeviceService.isDeviceTrusted.mockResolvedValue(true);
|
|
212
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Country exists
|
|
213
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // No previous session
|
|
214
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
215
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
216
|
+
|
|
217
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
218
|
+
|
|
219
|
+
expect(factors).not.toContain(RiskFactor.NEW_DEVICE);
|
|
220
|
+
expect(mockTrustedDeviceService.isDeviceTrusted).toHaveBeenCalledWith(mockClientInfo.deviceToken, mockUser.id);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should continue to session check if trusted device check fails', async () => {
|
|
224
|
+
mockTrustedDeviceService.isDeviceTrusted.mockRejectedValue(new Error('Trusted device service error'));
|
|
225
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // Device not found in sessions
|
|
226
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Country exists
|
|
227
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // No previous session
|
|
228
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
229
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
230
|
+
|
|
231
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
232
|
+
|
|
233
|
+
expect(factors).toContain(RiskFactor.NEW_DEVICE);
|
|
234
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should handle device check errors gracefully', async () => {
|
|
238
|
+
mockSessionRepository.findOne.mockRejectedValueOnce(new Error('Database error'));
|
|
239
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Country exists
|
|
240
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // No previous session
|
|
241
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
242
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
243
|
+
|
|
244
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
245
|
+
|
|
246
|
+
// Should assume new device on error (safer for security)
|
|
247
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// ============================================================================
|
|
252
|
+
// detectRiskFactors() - NEW_IP
|
|
253
|
+
// ============================================================================
|
|
254
|
+
|
|
255
|
+
describe('detectRiskFactors() - new_ip', () => {
|
|
256
|
+
it('should detect new IP when IP never seen before', async () => {
|
|
257
|
+
mockSessionRepository.findOne
|
|
258
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
259
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
260
|
+
.mockResolvedValueOnce(null) // No previous session for impossible_travel
|
|
261
|
+
.mockResolvedValueOnce(null); // IP not found in sessions
|
|
262
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null); // IP not found in audit
|
|
263
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null); // No suspicious activity
|
|
264
|
+
mockAuditRepository.find.mockResolvedValueOnce([]); // No failed logins
|
|
265
|
+
|
|
266
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
267
|
+
|
|
268
|
+
expect(factors).toContain(RiskFactor.NEW_IP);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should not detect new IP when IP seen in sessions', async () => {
|
|
272
|
+
mockSessionRepository.findOne
|
|
273
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
274
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
275
|
+
.mockResolvedValueOnce(null) // No previous session for impossible_travel
|
|
276
|
+
.mockResolvedValueOnce({ id: 1 } as any); // IP found in sessions
|
|
277
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
278
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
279
|
+
|
|
280
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
281
|
+
|
|
282
|
+
expect(factors).not.toContain(RiskFactor.NEW_IP);
|
|
283
|
+
// Should not check audit if found in sessions
|
|
284
|
+
expect(mockAuditRepository.findOne).not.toHaveBeenCalledWith(
|
|
285
|
+
(expect as any).objectContaining({
|
|
286
|
+
where: (expect as any).objectContaining({ ipAddress: mockClientInfo.ipAddress }),
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should check audit trail if not found in sessions', async () => {
|
|
292
|
+
mockSessionRepository.findOne
|
|
293
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
294
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
295
|
+
.mockResolvedValueOnce(null) // No previous session for impossible_travel
|
|
296
|
+
.mockResolvedValueOnce(null); // IP not found in sessions
|
|
297
|
+
// IP check in audit: IP found in audit (not new)
|
|
298
|
+
mockAuditRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // IP found in audit (not suspicious activity check)
|
|
299
|
+
// suspicious_activity checks
|
|
300
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null); // No suspicious events
|
|
301
|
+
mockAuditRepository.find.mockResolvedValueOnce([]); // No failed logins
|
|
302
|
+
|
|
303
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
304
|
+
|
|
305
|
+
expect(factors).not.toContain(RiskFactor.NEW_IP);
|
|
306
|
+
// Should check audit for IP (isNewIp) and for suspicious activity (detectSuspiciousActivity)
|
|
307
|
+
expect(mockAuditRepository.findOne).toHaveBeenCalledTimes(2);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should NOT detect new_ip when new_country is detected (double-counting prevention)', async () => {
|
|
311
|
+
// Setup: new_country detected
|
|
312
|
+
mockSessionRepository.findOne
|
|
313
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
314
|
+
.mockResolvedValueOnce(null) // Country not found (countryExists query)
|
|
315
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Sessions with country data exist (hasAnyCountryData)
|
|
316
|
+
.mockResolvedValueOnce(null); // No previous session for impossible_travel
|
|
317
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
318
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
319
|
+
|
|
320
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
321
|
+
|
|
322
|
+
expect(factors).toContain(RiskFactor.NEW_COUNTRY);
|
|
323
|
+
expect(factors).not.toContain(RiskFactor.NEW_IP);
|
|
324
|
+
// Should not check IP at all when country changed
|
|
325
|
+
expect(mockSessionRepository.findOne).not.toHaveBeenCalledWith(
|
|
326
|
+
(expect as any).objectContaining({
|
|
327
|
+
where: (expect as any).objectContaining({ ipAddress: mockClientInfo.ipAddress }),
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should NOT detect new_ip when impossible_travel is detected', async () => {
|
|
333
|
+
const lastSession = {
|
|
334
|
+
id: 1,
|
|
335
|
+
userId: 1,
|
|
336
|
+
ipCountry: 'GB',
|
|
337
|
+
ipCity: 'London',
|
|
338
|
+
lastActivityAt: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago
|
|
339
|
+
createdAt: new Date(),
|
|
340
|
+
} as ISession;
|
|
341
|
+
|
|
342
|
+
mockSessionRepository.findOne
|
|
343
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
344
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists (same country)
|
|
345
|
+
.mockResolvedValueOnce(lastSession as any); // Previous session found (impossible travel detected)
|
|
346
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
347
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
348
|
+
|
|
349
|
+
const clientInfo: ClientInfo = {
|
|
350
|
+
...mockClientInfo,
|
|
351
|
+
ipCountry: 'GB',
|
|
352
|
+
ipCity: 'Manchester', // Different city, same country
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfo);
|
|
356
|
+
|
|
357
|
+
// If impossible_travel is detected, new_ip should not be checked
|
|
358
|
+
expect(factors).not.toContain(RiskFactor.NEW_IP);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should skip new_ip check if trigger not enabled', async () => {
|
|
362
|
+
mockConfig.mfa!.adaptive!.triggers = [RiskFactor.NEW_DEVICE, RiskFactor.NEW_COUNTRY];
|
|
363
|
+
service = new RiskDetectionService(
|
|
364
|
+
mockSessionRepository,
|
|
365
|
+
mockAuditRepository,
|
|
366
|
+
mockConfig,
|
|
367
|
+
mockLogger,
|
|
368
|
+
mockTrustedDeviceService,
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
mockSessionRepository.findOne
|
|
372
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
373
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
374
|
+
.mockResolvedValueOnce(null); // No previous session
|
|
375
|
+
|
|
376
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
377
|
+
|
|
378
|
+
expect(factors).not.toContain(RiskFactor.NEW_IP);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should skip new_ip check if ipAddress not provided', async () => {
|
|
382
|
+
const clientInfoWithoutIp = { ...mockClientInfo, ipAddress: '' as any };
|
|
383
|
+
|
|
384
|
+
mockSessionRepository.findOne
|
|
385
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
386
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
387
|
+
.mockResolvedValueOnce(null); // No previous session
|
|
388
|
+
|
|
389
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoWithoutIp);
|
|
390
|
+
|
|
391
|
+
expect(factors).not.toContain(RiskFactor.NEW_IP);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should handle IP check errors gracefully', async () => {
|
|
395
|
+
mockSessionRepository.findOne
|
|
396
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
397
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
398
|
+
.mockResolvedValueOnce(null) // No previous session
|
|
399
|
+
.mockRejectedValueOnce(new Error('Database error')); // IP check fails
|
|
400
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
401
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
402
|
+
|
|
403
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
404
|
+
|
|
405
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
406
|
+
// Should assume new IP on error (safer for security)
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// ============================================================================
|
|
411
|
+
// detectRiskFactors() - NEW_COUNTRY
|
|
412
|
+
// ============================================================================
|
|
413
|
+
|
|
414
|
+
describe('detectRiskFactors() - new_country', () => {
|
|
415
|
+
it('should NOT detect new_country on first login (no previous sessions)', async () => {
|
|
416
|
+
// First login: no sessions exist, so new_country should not be flagged
|
|
417
|
+
mockSessionRepository.findOne
|
|
418
|
+
.mockResolvedValueOnce(null) // new_device: device not found (first login)
|
|
419
|
+
.mockResolvedValueOnce(null) // isNewCountry: country not found (countryExists query)
|
|
420
|
+
.mockResolvedValueOnce(null); // isNewCountry: no sessions with country data (can't determine)
|
|
421
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // No previous session for impossible_travel
|
|
422
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
423
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
424
|
+
|
|
425
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
426
|
+
|
|
427
|
+
// new_device should be detected, but new_country should NOT (no history to compare)
|
|
428
|
+
expect(factors).toContain(RiskFactor.NEW_DEVICE);
|
|
429
|
+
expect(factors).not.toContain(RiskFactor.NEW_COUNTRY);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should detect new country when country never seen before (user has previous sessions)', async () => {
|
|
433
|
+
mockSessionRepository.findOne
|
|
434
|
+
.mockResolvedValueOnce({ id: 1 } as any) // new_device: device exists
|
|
435
|
+
.mockResolvedValueOnce(null) // isNewCountry: country not found (countryExists query)
|
|
436
|
+
.mockResolvedValueOnce({ id: 1 } as any) // isNewCountry: sessions with country data exist (has country history, so country is new)
|
|
437
|
+
.mockResolvedValueOnce(null); // No previous session for impossible_travel
|
|
438
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
439
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
440
|
+
|
|
441
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
442
|
+
|
|
443
|
+
expect(factors).toContain(RiskFactor.NEW_COUNTRY);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('should not detect new country when country seen before', async () => {
|
|
447
|
+
// Optimized: country exists check returns early (1 query instead of 3)
|
|
448
|
+
mockSessionRepository.findOne
|
|
449
|
+
.mockResolvedValueOnce({ id: 1 } as any) // new_device: device exists
|
|
450
|
+
.mockResolvedValueOnce({ id: 1 } as any) // isNewCountry: country exists (returns false immediately, no second query needed)
|
|
451
|
+
.mockResolvedValueOnce(null); // No previous session for impossible_travel
|
|
452
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
453
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
454
|
+
|
|
455
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
456
|
+
|
|
457
|
+
expect(factors).not.toContain(RiskFactor.NEW_COUNTRY);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('should skip new_country check if trigger not enabled', async () => {
|
|
461
|
+
mockConfig.mfa!.adaptive!.triggers = [RiskFactor.NEW_DEVICE, RiskFactor.NEW_IP];
|
|
462
|
+
service = new RiskDetectionService(
|
|
463
|
+
mockSessionRepository,
|
|
464
|
+
mockAuditRepository,
|
|
465
|
+
mockConfig,
|
|
466
|
+
mockLogger,
|
|
467
|
+
mockTrustedDeviceService,
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Device exists
|
|
471
|
+
|
|
472
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
473
|
+
|
|
474
|
+
expect(factors).not.toContain(RiskFactor.NEW_COUNTRY);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should skip new_country check if ipCountry not provided', async () => {
|
|
478
|
+
const clientInfoWithoutCountry = { ...mockClientInfo, ipCountry: undefined };
|
|
479
|
+
|
|
480
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Device exists
|
|
481
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // No previous session
|
|
482
|
+
|
|
483
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoWithoutCountry);
|
|
484
|
+
|
|
485
|
+
expect(factors).not.toContain(RiskFactor.NEW_COUNTRY);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('should handle country check errors gracefully', async () => {
|
|
489
|
+
mockSessionRepository.findOne
|
|
490
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
491
|
+
.mockRejectedValueOnce(new Error('Database error')); // Country check fails
|
|
492
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // No previous session
|
|
493
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
494
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
495
|
+
|
|
496
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
497
|
+
|
|
498
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
499
|
+
// Should assume not new country on error (safer default)
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// ============================================================================
|
|
504
|
+
// detectRiskFactors() - IMPOSSIBLE_TRAVEL
|
|
505
|
+
// ============================================================================
|
|
506
|
+
|
|
507
|
+
describe('detectRiskFactors() - impossible_travel', () => {
|
|
508
|
+
it('should skip impossible_travel check if trigger not enabled', async () => {
|
|
509
|
+
mockConfig.mfa!.adaptive!.triggers = [RiskFactor.NEW_DEVICE];
|
|
510
|
+
service = new RiskDetectionService(
|
|
511
|
+
mockSessionRepository,
|
|
512
|
+
mockAuditRepository,
|
|
513
|
+
mockConfig,
|
|
514
|
+
mockLogger,
|
|
515
|
+
mockTrustedDeviceService,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Device exists
|
|
519
|
+
|
|
520
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
521
|
+
|
|
522
|
+
expect(factors).not.toContain(RiskFactor.IMPOSSIBLE_TRAVEL);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should skip impossible_travel check if location data incomplete', async () => {
|
|
526
|
+
const clientInfoWithoutCity = { ...mockClientInfo, ipCity: undefined };
|
|
527
|
+
|
|
528
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Device exists
|
|
529
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Country exists
|
|
530
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // IP exists
|
|
531
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
532
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
533
|
+
|
|
534
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoWithoutCity);
|
|
535
|
+
|
|
536
|
+
expect(factors).not.toContain(RiskFactor.IMPOSSIBLE_TRAVEL);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('should skip impossible_travel check if ipCountry missing', async () => {
|
|
540
|
+
const clientInfoWithoutCountry = { ...mockClientInfo, ipCountry: undefined };
|
|
541
|
+
|
|
542
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Device exists
|
|
543
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Country exists (but clientInfo doesn't have it)
|
|
544
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // IP exists
|
|
545
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
546
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
547
|
+
|
|
548
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoWithoutCountry);
|
|
549
|
+
|
|
550
|
+
expect(factors).not.toContain(RiskFactor.IMPOSSIBLE_TRAVEL);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('should return false for impossible_travel if no previous location', async () => {
|
|
554
|
+
mockSessionRepository.findOne
|
|
555
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
556
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
557
|
+
.mockResolvedValueOnce(null); // No previous session with location
|
|
558
|
+
|
|
559
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
560
|
+
|
|
561
|
+
expect(factors).not.toContain(RiskFactor.IMPOSSIBLE_TRAVEL);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('should not detect impossible_travel if same location', async () => {
|
|
565
|
+
const lastSession = {
|
|
566
|
+
id: 1,
|
|
567
|
+
userId: 1,
|
|
568
|
+
ipCountry: 'US',
|
|
569
|
+
ipCity: 'New York',
|
|
570
|
+
lastActivityAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
|
|
571
|
+
createdAt: new Date(),
|
|
572
|
+
} as ISession;
|
|
573
|
+
|
|
574
|
+
mockSessionRepository.findOne
|
|
575
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
576
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
577
|
+
.mockResolvedValueOnce(lastSession as any); // Previous session with same location
|
|
578
|
+
|
|
579
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
580
|
+
|
|
581
|
+
expect(factors).not.toContain(RiskFactor.IMPOSSIBLE_TRAVEL);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('should detect impossible_travel when travel speed exceeds threshold', async () => {
|
|
585
|
+
const lastSession = {
|
|
586
|
+
id: 1,
|
|
587
|
+
userId: 1,
|
|
588
|
+
ipCountry: 'US',
|
|
589
|
+
ipCity: 'New York',
|
|
590
|
+
lastActivityAt: new Date(Date.now() - 15 * 60 * 1000), // 15 minutes ago
|
|
591
|
+
createdAt: new Date(Date.now() - 15 * 60 * 1000), // 15 minutes ago (login time)
|
|
592
|
+
} as ISession;
|
|
593
|
+
|
|
594
|
+
const clientInfoDifferentCity: ClientInfo = {
|
|
595
|
+
...mockClientInfo,
|
|
596
|
+
ipCountry: 'US',
|
|
597
|
+
ipCity: 'Los Angeles', // Different city, same country
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
mockSessionRepository.findOne
|
|
601
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
602
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
603
|
+
.mockResolvedValueOnce(lastSession as any); // Previous session with different city
|
|
604
|
+
|
|
605
|
+
// Distance calculation: same country, different city = 500 km
|
|
606
|
+
// Time: 15 minutes = 0.25 hours
|
|
607
|
+
// Speed: 500 / 0.25 = 2000 km/h > 900 km/h (default threshold) -> impossible travel
|
|
608
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoDifferentCity);
|
|
609
|
+
|
|
610
|
+
expect(factors).toContain(RiskFactor.IMPOSSIBLE_TRAVEL);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('should not detect impossible_travel when distance is 0 (same location)', async () => {
|
|
614
|
+
const lastSession = {
|
|
615
|
+
id: 1,
|
|
616
|
+
userId: 1,
|
|
617
|
+
ipCountry: 'US',
|
|
618
|
+
ipCity: 'New York',
|
|
619
|
+
lastActivityAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
|
|
620
|
+
createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago (login time)
|
|
621
|
+
} as ISession;
|
|
622
|
+
|
|
623
|
+
// Same city and country (distance = 0)
|
|
624
|
+
const clientInfoSameLocation: ClientInfo = {
|
|
625
|
+
...mockClientInfo,
|
|
626
|
+
ipCountry: 'US',
|
|
627
|
+
ipCity: 'New York', // Same city
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
mockSessionRepository.findOne
|
|
631
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
632
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
633
|
+
.mockResolvedValueOnce(lastSession as any); // Previous session with same location
|
|
634
|
+
|
|
635
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoSameLocation);
|
|
636
|
+
|
|
637
|
+
// calculateDistance returns 0, so impossible_travel should not be detected
|
|
638
|
+
expect(factors).not.toContain(RiskFactor.IMPOSSIBLE_TRAVEL);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('should detect impossible_travel for different countries', async () => {
|
|
642
|
+
const lastSession = {
|
|
643
|
+
id: 1,
|
|
644
|
+
userId: 1,
|
|
645
|
+
ipCountry: 'US',
|
|
646
|
+
ipCity: 'New York',
|
|
647
|
+
lastActivityAt: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago
|
|
648
|
+
createdAt: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago (login time)
|
|
649
|
+
} as ISession;
|
|
650
|
+
|
|
651
|
+
// Different country (distance = 2000 km)
|
|
652
|
+
const clientInfoDifferentCountry: ClientInfo = {
|
|
653
|
+
...mockClientInfo,
|
|
654
|
+
ipCountry: 'GB',
|
|
655
|
+
ipCity: 'London',
|
|
656
|
+
ipLatitude: 51.5074,
|
|
657
|
+
ipLongitude: -0.1278,
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
mockSessionRepository.findOne
|
|
661
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
662
|
+
.mockResolvedValueOnce(null) // Country doesn't exist (new country)
|
|
663
|
+
.mockResolvedValueOnce({ id: 1 } as any) // hasAnyCountryData check
|
|
664
|
+
.mockResolvedValueOnce(lastSession as any) // Previous session for impossible_travel
|
|
665
|
+
.mockResolvedValueOnce({ id: 1 } as any); // hasUserLoggedInBefore for INCOMPLETE_LOCATION_DATA
|
|
666
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null); // No previous audit login
|
|
667
|
+
|
|
668
|
+
// Distance: 2000 km, Time: 30 minutes = 0.5 hours
|
|
669
|
+
// Speed: 2000 / 0.5 = 4000 km/h > 900 km/h -> impossible travel
|
|
670
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoDifferentCountry);
|
|
671
|
+
|
|
672
|
+
expect(factors).toContain(RiskFactor.IMPOSSIBLE_TRAVEL);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('should detect impossible_travel when time difference is less than 30 minutes', async () => {
|
|
676
|
+
const lastSession = {
|
|
677
|
+
id: 1,
|
|
678
|
+
userId: 1,
|
|
679
|
+
ipCountry: 'US',
|
|
680
|
+
ipCity: 'New York',
|
|
681
|
+
lastActivityAt: new Date(Date.now() - 20 * 60 * 1000), // 20 minutes ago
|
|
682
|
+
createdAt: new Date(Date.now() - 20 * 60 * 1000), // 20 minutes ago (login time)
|
|
683
|
+
} as ISession;
|
|
684
|
+
|
|
685
|
+
const clientInfoDifferentCity: ClientInfo = {
|
|
686
|
+
...mockClientInfo,
|
|
687
|
+
ipCountry: 'US',
|
|
688
|
+
ipCity: 'Los Angeles',
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
mockSessionRepository.findOne
|
|
692
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
693
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
694
|
+
.mockResolvedValueOnce(lastSession as any);
|
|
695
|
+
|
|
696
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoDifferentCity);
|
|
697
|
+
|
|
698
|
+
// Time < 30 minutes with different city = impossible travel
|
|
699
|
+
expect(factors).toContain(RiskFactor.IMPOSSIBLE_TRAVEL);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it('should use custom maxTravelSpeed from config', async () => {
|
|
703
|
+
mockConfig.mfa!.adaptive!.maxTravelSpeed = 500; // Lower threshold
|
|
704
|
+
|
|
705
|
+
const lastSession = {
|
|
706
|
+
id: 1,
|
|
707
|
+
userId: 1,
|
|
708
|
+
ipCountry: 'US',
|
|
709
|
+
ipCity: 'New York',
|
|
710
|
+
lastActivityAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
|
|
711
|
+
createdAt: new Date(),
|
|
712
|
+
} as ISession;
|
|
713
|
+
|
|
714
|
+
const clientInfoDifferentCity: ClientInfo = {
|
|
715
|
+
...mockClientInfo,
|
|
716
|
+
ipCountry: 'US',
|
|
717
|
+
ipCity: 'Los Angeles',
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
service = new RiskDetectionService(
|
|
721
|
+
mockSessionRepository,
|
|
722
|
+
mockAuditRepository,
|
|
723
|
+
mockConfig,
|
|
724
|
+
mockLogger,
|
|
725
|
+
mockTrustedDeviceService,
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
mockSessionRepository.findOne
|
|
729
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
730
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
731
|
+
.mockResolvedValueOnce(lastSession as any);
|
|
732
|
+
|
|
733
|
+
// Distance: 500 km, Time: 2 hours, Speed: 250 km/h < 500 km/h -> not impossible
|
|
734
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoDifferentCity);
|
|
735
|
+
|
|
736
|
+
expect(factors).not.toContain(RiskFactor.IMPOSSIBLE_TRAVEL);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('should handle impossible travel check errors gracefully', async () => {
|
|
740
|
+
mockSessionRepository.findOne
|
|
741
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
742
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
743
|
+
.mockRejectedValueOnce(new Error('Database error')); // Impossible travel check fails
|
|
744
|
+
|
|
745
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
746
|
+
|
|
747
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
748
|
+
// Should assume not impossible travel on error
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it('should detect impossible_travel using Haversine formula when coordinates are available', async () => {
|
|
752
|
+
// Singapore to London: ~10,850 km in 10 minutes = impossible
|
|
753
|
+
const lastSession = {
|
|
754
|
+
id: 1,
|
|
755
|
+
userId: 1,
|
|
756
|
+
ipCountry: 'SG',
|
|
757
|
+
ipCity: 'Singapore',
|
|
758
|
+
ipLatitude: 1.3521,
|
|
759
|
+
ipLongitude: 103.8198,
|
|
760
|
+
createdAt: new Date(Date.now() - 10 * 60 * 1000), // 10 minutes ago
|
|
761
|
+
} as any;
|
|
762
|
+
|
|
763
|
+
const clientInfoWithCoordinates: ClientInfo = {
|
|
764
|
+
...mockClientInfo,
|
|
765
|
+
ipCountry: 'GB',
|
|
766
|
+
ipCity: 'London',
|
|
767
|
+
ipLatitude: 51.5074,
|
|
768
|
+
ipLongitude: -0.1278,
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
mockSessionRepository.findOne
|
|
772
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
773
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
774
|
+
.mockResolvedValueOnce(lastSession); // Coordinates included
|
|
775
|
+
|
|
776
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoWithCoordinates);
|
|
777
|
+
|
|
778
|
+
// Distance: ~10,850 km (Haversine), Time: 10 minutes = 0.167 hours
|
|
779
|
+
// Speed: ~65,000 km/h >> 900 km/h -> impossible travel
|
|
780
|
+
expect(factors).toContain(RiskFactor.IMPOSSIBLE_TRAVEL);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it('should detect impossible_travel when country changes with missing city data (conservative)', async () => {
|
|
784
|
+
// Country changed but no city data - should flag if < 2 hours
|
|
785
|
+
const lastSession = {
|
|
786
|
+
id: 1,
|
|
787
|
+
userId: 1,
|
|
788
|
+
ipCountry: 'SG',
|
|
789
|
+
ipCity: null, // Missing city
|
|
790
|
+
createdAt: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago
|
|
791
|
+
} as ISession;
|
|
792
|
+
|
|
793
|
+
const clientInfoMissingCity: ClientInfo = {
|
|
794
|
+
...mockClientInfo,
|
|
795
|
+
ipCountry: 'NO', // Different country
|
|
796
|
+
ipCity: undefined, // Missing city
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
mockSessionRepository.findOne
|
|
800
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
801
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
802
|
+
.mockResolvedValueOnce(lastSession as any);
|
|
803
|
+
|
|
804
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoMissingCity);
|
|
805
|
+
|
|
806
|
+
// Country changed in < 2 hours without city data -> flag as impossible
|
|
807
|
+
expect(factors).toContain(RiskFactor.IMPOSSIBLE_TRAVEL);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it('should NOT detect impossible_travel when country changes with missing city data but enough time passed', async () => {
|
|
811
|
+
// Country changed but no city data - should NOT flag if > 2 hours
|
|
812
|
+
const lastSession = {
|
|
813
|
+
id: 1,
|
|
814
|
+
userId: 1,
|
|
815
|
+
ipCountry: 'SG',
|
|
816
|
+
ipCity: null,
|
|
817
|
+
createdAt: new Date(Date.now() - 3 * 60 * 60 * 1000), // 3 hours ago
|
|
818
|
+
} as ISession;
|
|
819
|
+
|
|
820
|
+
const clientInfoMissingCity: ClientInfo = {
|
|
821
|
+
...mockClientInfo,
|
|
822
|
+
ipCountry: 'NO',
|
|
823
|
+
ipCity: undefined,
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
mockSessionRepository.findOne
|
|
827
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
828
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
829
|
+
.mockResolvedValueOnce(lastSession as any);
|
|
830
|
+
|
|
831
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoMissingCity);
|
|
832
|
+
|
|
833
|
+
// Country changed but > 2 hours passed -> acceptable
|
|
834
|
+
expect(factors).not.toContain(RiskFactor.IMPOSSIBLE_TRAVEL);
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// ============================================================================
|
|
839
|
+
// detectRiskFactors() - INCOMPLETE_LOCATION_DATA
|
|
840
|
+
// ============================================================================
|
|
841
|
+
|
|
842
|
+
describe('detectRiskFactors() - incomplete_location_data', () => {
|
|
843
|
+
it('should detect incomplete_location_data when city is missing', async () => {
|
|
844
|
+
const clientInfoMissingCity: ClientInfo = {
|
|
845
|
+
...mockClientInfo,
|
|
846
|
+
ipCountry: 'US',
|
|
847
|
+
ipCity: undefined,
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
mockSessionRepository.findOne
|
|
851
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
852
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
853
|
+
.mockResolvedValueOnce({ id: 1 } as any); // hasUserLoggedInBefore
|
|
854
|
+
|
|
855
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoMissingCity);
|
|
856
|
+
|
|
857
|
+
expect(factors).toContain(RiskFactor.INCOMPLETE_LOCATION_DATA);
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
it('should detect incomplete_location_data when coordinates are missing', async () => {
|
|
861
|
+
const clientInfoMissingCoordinates: ClientInfo = {
|
|
862
|
+
...mockClientInfo,
|
|
863
|
+
ipCountry: 'US',
|
|
864
|
+
ipCity: 'New York',
|
|
865
|
+
ipLatitude: undefined,
|
|
866
|
+
ipLongitude: undefined,
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
mockSessionRepository.findOne
|
|
870
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
871
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
872
|
+
.mockResolvedValueOnce({ id: 1 } as any); // hasUserLoggedInBefore
|
|
873
|
+
|
|
874
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoMissingCoordinates);
|
|
875
|
+
|
|
876
|
+
expect(factors).toContain(RiskFactor.INCOMPLETE_LOCATION_DATA);
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it('should NOT detect incomplete_location_data when all location data is present', async () => {
|
|
880
|
+
const clientInfoComplete: ClientInfo = {
|
|
881
|
+
...mockClientInfo,
|
|
882
|
+
ipCountry: 'US',
|
|
883
|
+
ipCity: 'New York',
|
|
884
|
+
ipLatitude: 40.7128,
|
|
885
|
+
ipLongitude: -74.006,
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
mockSessionRepository.findOne
|
|
889
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
890
|
+
.mockResolvedValueOnce({ id: 1 } as any); // Country exists
|
|
891
|
+
|
|
892
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoComplete);
|
|
893
|
+
|
|
894
|
+
expect(factors).not.toContain(RiskFactor.INCOMPLETE_LOCATION_DATA);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it('should NOT detect incomplete_location_data when country is missing', async () => {
|
|
898
|
+
const clientInfoNoCountry: ClientInfo = {
|
|
899
|
+
...mockClientInfo,
|
|
900
|
+
ipCountry: undefined,
|
|
901
|
+
ipCity: undefined,
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Device exists
|
|
905
|
+
|
|
906
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoNoCountry);
|
|
907
|
+
|
|
908
|
+
// Should not add incomplete_location_data when there's no country at all
|
|
909
|
+
expect(factors).not.toContain(RiskFactor.INCOMPLETE_LOCATION_DATA);
|
|
910
|
+
});
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
// ============================================================================
|
|
914
|
+
// detectRiskFactors() - SUSPICIOUS_ACTIVITY
|
|
915
|
+
// ============================================================================
|
|
916
|
+
|
|
917
|
+
describe('detectRiskFactors() - suspicious_activity', () => {
|
|
918
|
+
it('should detect suspicious_activity when recent suspicious events found', async () => {
|
|
919
|
+
// Ensure complete location data to avoid INCOMPLETE_LOCATION_DATA
|
|
920
|
+
const clientInfoComplete: ClientInfo = {
|
|
921
|
+
...mockClientInfo,
|
|
922
|
+
ipCountry: 'US',
|
|
923
|
+
ipCity: 'New York',
|
|
924
|
+
ipLatitude: 40.7128,
|
|
925
|
+
ipLongitude: -74.006,
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
mockSessionRepository.findOne
|
|
929
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
930
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
931
|
+
.mockResolvedValueOnce(null) // No previous session for impossible_travel
|
|
932
|
+
.mockResolvedValueOnce({ id: 1 } as any); // IP exists (no new_ip)
|
|
933
|
+
// suspicious_activity check: findOne for suspicious events
|
|
934
|
+
mockAuditRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Suspicious events found
|
|
935
|
+
mockAuditRepository.find.mockResolvedValueOnce([]); // Failed logins (not needed if suspicious found)
|
|
936
|
+
|
|
937
|
+
const factors = await service.detectRiskFactors(mockUser, clientInfoComplete);
|
|
938
|
+
|
|
939
|
+
expect(factors).toContain(RiskFactor.SUSPICIOUS_ACTIVITY);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
it('should detect suspicious_activity when 3+ failed logins in last hour', async () => {
|
|
943
|
+
mockSessionRepository.findOne
|
|
944
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
945
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
946
|
+
.mockResolvedValueOnce(null); // No previous session
|
|
947
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null); // No suspicious events
|
|
948
|
+
mockAuditRepository.find.mockResolvedValueOnce([{ id: 1 } as any, { id: 2 } as any, { id: 3 } as any]); // 3 failed logins
|
|
949
|
+
|
|
950
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
951
|
+
|
|
952
|
+
expect(factors).toContain(RiskFactor.SUSPICIOUS_ACTIVITY);
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
it('should not detect suspicious_activity when threshold not met', async () => {
|
|
956
|
+
mockSessionRepository.findOne
|
|
957
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
958
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
959
|
+
.mockResolvedValueOnce(null); // No previous session
|
|
960
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null); // No suspicious events
|
|
961
|
+
mockAuditRepository.find.mockResolvedValueOnce([{ id: 1 } as any, { id: 2 } as any]); // Only 2 failed logins (below threshold)
|
|
962
|
+
|
|
963
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
964
|
+
|
|
965
|
+
expect(factors).not.toContain(RiskFactor.SUSPICIOUS_ACTIVITY);
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it('should use custom suspiciousActivityWindow from config', async () => {
|
|
969
|
+
mockConfig.mfa!.adaptive!.suspiciousActivityWindow = 2; // 2 hours instead of 1
|
|
970
|
+
|
|
971
|
+
service = new RiskDetectionService(
|
|
972
|
+
mockSessionRepository,
|
|
973
|
+
mockAuditRepository,
|
|
974
|
+
mockConfig,
|
|
975
|
+
mockLogger,
|
|
976
|
+
mockTrustedDeviceService,
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
mockSessionRepository.findOne
|
|
980
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
981
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
982
|
+
.mockResolvedValueOnce(null); // No previous session
|
|
983
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
984
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
985
|
+
|
|
986
|
+
await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
987
|
+
|
|
988
|
+
// Verify find was called with correct time window (2 hours ago)
|
|
989
|
+
expect(mockAuditRepository.find).toHaveBeenCalled();
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
it('should skip suspicious_activity check if trigger not enabled', async () => {
|
|
993
|
+
mockConfig.mfa!.adaptive!.triggers = [RiskFactor.NEW_DEVICE];
|
|
994
|
+
service = new RiskDetectionService(
|
|
995
|
+
mockSessionRepository,
|
|
996
|
+
mockAuditRepository,
|
|
997
|
+
mockConfig,
|
|
998
|
+
mockLogger,
|
|
999
|
+
mockTrustedDeviceService,
|
|
1000
|
+
);
|
|
1001
|
+
|
|
1002
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Device exists
|
|
1003
|
+
|
|
1004
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
1005
|
+
|
|
1006
|
+
expect(factors).not.toContain(RiskFactor.SUSPICIOUS_ACTIVITY);
|
|
1007
|
+
expect(mockAuditRepository.findOne).not.toHaveBeenCalled();
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it('should handle suspicious activity check errors gracefully', async () => {
|
|
1011
|
+
mockSessionRepository.findOne
|
|
1012
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
1013
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
1014
|
+
.mockResolvedValueOnce(null); // No previous session
|
|
1015
|
+
mockAuditRepository.findOne.mockRejectedValueOnce(new Error('Database error'));
|
|
1016
|
+
|
|
1017
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
1018
|
+
|
|
1019
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
1020
|
+
// Should assume not suspicious on error
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// ============================================================================
|
|
1025
|
+
// detectRiskFactors() - Error Handling
|
|
1026
|
+
// ============================================================================
|
|
1027
|
+
|
|
1028
|
+
describe('detectRiskFactors() - error handling', () => {
|
|
1029
|
+
it('should handle errors in individual checks gracefully', async () => {
|
|
1030
|
+
// First check succeeds, second fails
|
|
1031
|
+
mockSessionRepository.findOne
|
|
1032
|
+
.mockResolvedValueOnce(null) // new_device succeeds (device not found)
|
|
1033
|
+
.mockRejectedValueOnce(new Error('Database error')); // isNewCountry: countryExists query fails
|
|
1034
|
+
|
|
1035
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
1036
|
+
|
|
1037
|
+
// Should still detect new_device, but handle error for new_country
|
|
1038
|
+
expect(factors).toContain(RiskFactor.NEW_DEVICE);
|
|
1039
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
it('should handle errors in isNewDevice gracefully', async () => {
|
|
1043
|
+
mockSessionRepository.findOne.mockRejectedValueOnce(new Error('Database error'));
|
|
1044
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Country exists
|
|
1045
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // No previous session
|
|
1046
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
1047
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
1048
|
+
|
|
1049
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
1050
|
+
|
|
1051
|
+
// Should assume new device on error (safer for security)
|
|
1052
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
it('should handle errors in isNewIp gracefully', async () => {
|
|
1056
|
+
mockSessionRepository.findOne
|
|
1057
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
1058
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
1059
|
+
.mockResolvedValueOnce(null) // No previous session
|
|
1060
|
+
.mockRejectedValueOnce(new Error('Database error')); // IP check fails
|
|
1061
|
+
|
|
1062
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
1063
|
+
|
|
1064
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
it('should handle errors in isNewCountry gracefully', async () => {
|
|
1068
|
+
mockSessionRepository.findOne
|
|
1069
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
1070
|
+
.mockRejectedValueOnce(new Error('Database error')); // Country check fails
|
|
1071
|
+
|
|
1072
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
1073
|
+
|
|
1074
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
1075
|
+
// Should assume not new country on error (safer default)
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it('should handle errors in detectImpossibleTravel gracefully', async () => {
|
|
1079
|
+
mockSessionRepository.findOne
|
|
1080
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
1081
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
1082
|
+
.mockRejectedValueOnce(new Error('Database error')); // Impossible travel check fails
|
|
1083
|
+
|
|
1084
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
1085
|
+
|
|
1086
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
it('should handle errors in detectSuspiciousActivity gracefully', async () => {
|
|
1090
|
+
mockSessionRepository.findOne
|
|
1091
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Device exists
|
|
1092
|
+
.mockResolvedValueOnce({ id: 1 } as any) // Country exists
|
|
1093
|
+
.mockResolvedValueOnce(null); // No previous session
|
|
1094
|
+
mockAuditRepository.findOne.mockRejectedValueOnce(new Error('Database error'));
|
|
1095
|
+
|
|
1096
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
1097
|
+
|
|
1098
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
it('should handle non-Error exceptions in individual checks', async () => {
|
|
1102
|
+
mockSessionRepository.findOne
|
|
1103
|
+
.mockResolvedValueOnce(null) // new_device succeeds
|
|
1104
|
+
.mockRejectedValueOnce('String error' as any); // isNewCountry fails with non-Error
|
|
1105
|
+
|
|
1106
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
1107
|
+
|
|
1108
|
+
expect(factors).toContain(RiskFactor.NEW_DEVICE);
|
|
1109
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
it('should return empty array on outer catch block error', async () => {
|
|
1113
|
+
// Make user.sub throw an error when accessed in the catch block
|
|
1114
|
+
const userWithError = {
|
|
1115
|
+
...mockUser,
|
|
1116
|
+
get sub() {
|
|
1117
|
+
throw new Error('Accessing sub throws');
|
|
1118
|
+
},
|
|
1119
|
+
} as any;
|
|
1120
|
+
|
|
1121
|
+
// Make the first repository call throw to trigger outer catch
|
|
1122
|
+
// But individual checks have try-catch, so we need to make something else throw
|
|
1123
|
+
// Actually, if we make the config access throw, it would work
|
|
1124
|
+
// But a simpler approach: make the service throw during enabledTriggers access
|
|
1125
|
+
// by making the config property throw
|
|
1126
|
+
const throwingConfig = {
|
|
1127
|
+
...mockConfig,
|
|
1128
|
+
get mfa() {
|
|
1129
|
+
throw new Error('Config access error');
|
|
1130
|
+
},
|
|
1131
|
+
} as any;
|
|
1132
|
+
|
|
1133
|
+
const serviceWithError = new RiskDetectionService(
|
|
1134
|
+
mockSessionRepository,
|
|
1135
|
+
mockAuditRepository,
|
|
1136
|
+
throwingConfig,
|
|
1137
|
+
mockLogger,
|
|
1138
|
+
mockTrustedDeviceService,
|
|
1139
|
+
);
|
|
1140
|
+
|
|
1141
|
+
const factors = await serviceWithError.detectRiskFactors(mockUser, mockClientInfo);
|
|
1142
|
+
|
|
1143
|
+
expect(factors).toEqual([]);
|
|
1144
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
it('should handle non-Error in outer catch block', async () => {
|
|
1148
|
+
const throwingConfig = {
|
|
1149
|
+
...mockConfig,
|
|
1150
|
+
get mfa() {
|
|
1151
|
+
// eslint-disable-next-line no-throw-literal
|
|
1152
|
+
throw 'String error';
|
|
1153
|
+
},
|
|
1154
|
+
} as any;
|
|
1155
|
+
|
|
1156
|
+
const serviceWithError = new RiskDetectionService(
|
|
1157
|
+
mockSessionRepository,
|
|
1158
|
+
mockAuditRepository,
|
|
1159
|
+
throwingConfig,
|
|
1160
|
+
mockLogger,
|
|
1161
|
+
mockTrustedDeviceService,
|
|
1162
|
+
);
|
|
1163
|
+
|
|
1164
|
+
const factors = await serviceWithError.detectRiskFactors(mockUser, mockClientInfo);
|
|
1165
|
+
|
|
1166
|
+
expect(factors).toEqual([]);
|
|
1167
|
+
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
1168
|
+
(expect as any).stringContaining('Risk detection failed'),
|
|
1169
|
+
(expect as any).any(Object),
|
|
1170
|
+
);
|
|
1171
|
+
});
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
// ============================================================================
|
|
1175
|
+
// detectRiskFactors() - Configuration
|
|
1176
|
+
// ============================================================================
|
|
1177
|
+
|
|
1178
|
+
describe('detectRiskFactors() - configuration', () => {
|
|
1179
|
+
it('should use default triggers when not configured', async () => {
|
|
1180
|
+
mockConfig.mfa = undefined;
|
|
1181
|
+
service = new RiskDetectionService(
|
|
1182
|
+
mockSessionRepository,
|
|
1183
|
+
mockAuditRepository,
|
|
1184
|
+
mockConfig,
|
|
1185
|
+
mockLogger,
|
|
1186
|
+
mockTrustedDeviceService,
|
|
1187
|
+
);
|
|
1188
|
+
|
|
1189
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // Device not found
|
|
1190
|
+
|
|
1191
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
1192
|
+
|
|
1193
|
+
// Should check default triggers: new_device, new_ip, new_country
|
|
1194
|
+
expect(mockSessionRepository.findOne).toHaveBeenCalled();
|
|
1195
|
+
expect(factors).toContain(RiskFactor.NEW_DEVICE);
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
it('should respect custom trigger configuration', async () => {
|
|
1199
|
+
mockConfig.mfa!.adaptive!.triggers = [RiskFactor.NEW_DEVICE];
|
|
1200
|
+
service = new RiskDetectionService(
|
|
1201
|
+
mockSessionRepository,
|
|
1202
|
+
mockAuditRepository,
|
|
1203
|
+
mockConfig,
|
|
1204
|
+
mockLogger,
|
|
1205
|
+
mockTrustedDeviceService,
|
|
1206
|
+
);
|
|
1207
|
+
|
|
1208
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // Device not found
|
|
1209
|
+
|
|
1210
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
1211
|
+
|
|
1212
|
+
expect(factors).toContain(RiskFactor.NEW_DEVICE);
|
|
1213
|
+
// Should not check other triggers
|
|
1214
|
+
expect(mockSessionRepository.findOne).toHaveBeenCalledTimes(1); // Only new_device
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
it('should handle service without trusted device service', async () => {
|
|
1218
|
+
service = new RiskDetectionService(
|
|
1219
|
+
mockSessionRepository,
|
|
1220
|
+
mockAuditRepository,
|
|
1221
|
+
mockConfig,
|
|
1222
|
+
mockLogger,
|
|
1223
|
+
undefined, // No trusted device service
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // Device not found
|
|
1227
|
+
mockSessionRepository.findOne.mockResolvedValueOnce({ id: 1 } as any); // Country exists
|
|
1228
|
+
mockSessionRepository.findOne.mockResolvedValueOnce(null); // No previous session
|
|
1229
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
1230
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
1231
|
+
|
|
1232
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
1233
|
+
|
|
1234
|
+
expect(factors).toContain(RiskFactor.NEW_DEVICE);
|
|
1235
|
+
// Should not throw error
|
|
1236
|
+
});
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
// ============================================================================
|
|
1240
|
+
// Edge Cases and Integration
|
|
1241
|
+
// ============================================================================
|
|
1242
|
+
|
|
1243
|
+
describe('Edge Cases and Integration', () => {
|
|
1244
|
+
it('should handle multiple risk factors detected together', async () => {
|
|
1245
|
+
mockSessionRepository.findOne
|
|
1246
|
+
.mockResolvedValueOnce(null) // new_device: device not found
|
|
1247
|
+
.mockResolvedValueOnce(null) // new_country: country not found
|
|
1248
|
+
.mockResolvedValueOnce({ id: 1 } as any) // new_country: has country history
|
|
1249
|
+
.mockResolvedValueOnce(null); // No previous session for impossible_travel
|
|
1250
|
+
mockAuditRepository.findOne.mockResolvedValueOnce(null);
|
|
1251
|
+
mockAuditRepository.find.mockResolvedValueOnce([]);
|
|
1252
|
+
|
|
1253
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
1254
|
+
|
|
1255
|
+
expect(factors).toContain(RiskFactor.NEW_DEVICE);
|
|
1256
|
+
expect(factors).toContain(RiskFactor.NEW_COUNTRY);
|
|
1257
|
+
expect(factors).not.toContain(RiskFactor.NEW_IP); // Should be excluded due to new_country
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
it('should handle all risk factors disabled', async () => {
|
|
1261
|
+
mockConfig.mfa!.adaptive!.triggers = [];
|
|
1262
|
+
service = new RiskDetectionService(
|
|
1263
|
+
mockSessionRepository,
|
|
1264
|
+
mockAuditRepository,
|
|
1265
|
+
mockConfig,
|
|
1266
|
+
mockLogger,
|
|
1267
|
+
mockTrustedDeviceService,
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1270
|
+
const factors = await service.detectRiskFactors(mockUser, mockClientInfo);
|
|
1271
|
+
|
|
1272
|
+
expect(factors).toEqual([]);
|
|
1273
|
+
expect(mockSessionRepository.findOne).not.toHaveBeenCalled();
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
it('should handle concurrent risk detection calls', async () => {
|
|
1277
|
+
mockSessionRepository.findOne.mockResolvedValue({ id: 1 } as any);
|
|
1278
|
+
mockAuditRepository.findOne.mockResolvedValue(null);
|
|
1279
|
+
mockAuditRepository.find.mockResolvedValue([]);
|
|
1280
|
+
|
|
1281
|
+
const promises = [
|
|
1282
|
+
service.detectRiskFactors(mockUser, mockClientInfo),
|
|
1283
|
+
service.detectRiskFactors(mockUser, mockClientInfo),
|
|
1284
|
+
];
|
|
1285
|
+
|
|
1286
|
+
const results = await Promise.all(promises);
|
|
1287
|
+
|
|
1288
|
+
expect(results.length).toBe(2);
|
|
1289
|
+
expect(results[0]).toEqual(results[1]);
|
|
1290
|
+
});
|
|
1291
|
+
});
|
|
1292
|
+
});
|