@nauth-toolkit/core 0.1.14 → 0.1.17

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.
Files changed (623) hide show
  1. package/dist/adapters/database-columns.d.ts +70 -0
  2. package/dist/adapters/database-columns.d.ts.map +1 -1
  3. package/dist/adapters/database-columns.js +76 -2
  4. package/dist/adapters/database-columns.js.map +1 -1
  5. package/dist/adapters/express.adapter.d.ts +66 -0
  6. package/dist/adapters/express.adapter.d.ts.map +1 -1
  7. package/dist/adapters/express.adapter.js +80 -0
  8. package/dist/adapters/express.adapter.js.map +1 -1
  9. package/dist/adapters/fastify.adapter.d.ts +42 -0
  10. package/dist/adapters/fastify.adapter.d.ts.map +1 -1
  11. package/dist/adapters/fastify.adapter.js +86 -0
  12. package/dist/adapters/fastify.adapter.js.map +1 -1
  13. package/dist/adapters/index.d.ts +5 -0
  14. package/dist/adapters/index.d.ts.map +1 -1
  15. package/dist/adapters/index.js +9 -0
  16. package/dist/adapters/index.js.map +1 -1
  17. package/dist/adapters/storage.factory.d.ts +107 -0
  18. package/dist/adapters/storage.factory.d.ts.map +1 -1
  19. package/dist/adapters/storage.factory.js +114 -0
  20. package/dist/adapters/storage.factory.js.map +1 -1
  21. package/dist/adapters.d.ts +8 -0
  22. package/dist/adapters.d.ts.map +1 -1
  23. package/dist/adapters.js +8 -0
  24. package/dist/adapters.js.map +1 -1
  25. package/dist/bootstrap.d.ts +82 -0
  26. package/dist/bootstrap.d.ts.map +1 -1
  27. package/dist/bootstrap.js +106 -0
  28. package/dist/bootstrap.js.map +1 -1
  29. package/dist/dto/admin-set-password.dto.d.ts +90 -0
  30. package/dist/dto/admin-set-password.dto.d.ts.map +1 -1
  31. package/dist/dto/admin-set-password.dto.js +91 -0
  32. package/dist/dto/admin-set-password.dto.js.map +1 -1
  33. package/dist/dto/auth-challenge.dto.d.ts +170 -0
  34. package/dist/dto/auth-challenge.dto.d.ts.map +1 -1
  35. package/dist/dto/auth-challenge.dto.js +170 -0
  36. package/dist/dto/auth-challenge.dto.js.map +1 -1
  37. package/dist/dto/auth-response.dto.d.ts +196 -0
  38. package/dist/dto/auth-response.dto.d.ts.map +1 -1
  39. package/dist/dto/auth-response.dto.js +149 -0
  40. package/dist/dto/auth-response.dto.js.map +1 -1
  41. package/dist/dto/challenge-response.dto.d.ts +155 -0
  42. package/dist/dto/challenge-response.dto.d.ts.map +1 -1
  43. package/dist/dto/challenge-response.dto.js +8 -0
  44. package/dist/dto/challenge-response.dto.js.map +1 -1
  45. package/dist/dto/change-password-request.dto.d.ts +35 -0
  46. package/dist/dto/change-password-request.dto.d.ts.map +1 -1
  47. package/dist/dto/change-password-request.dto.js +35 -0
  48. package/dist/dto/change-password-request.dto.js.map +1 -1
  49. package/dist/dto/change-password-response.dto.d.ts +25 -0
  50. package/dist/dto/change-password-response.dto.d.ts.map +1 -1
  51. package/dist/dto/change-password-response.dto.js +25 -0
  52. package/dist/dto/change-password-response.dto.js.map +1 -1
  53. package/dist/dto/change-password.dto.d.ts +45 -0
  54. package/dist/dto/change-password.dto.d.ts.map +1 -1
  55. package/dist/dto/change-password.dto.js +45 -0
  56. package/dist/dto/change-password.dto.js.map +1 -1
  57. package/dist/dto/confirm-forgot-password.dto.d.ts +59 -0
  58. package/dist/dto/confirm-forgot-password.dto.d.ts.map +1 -1
  59. package/dist/dto/confirm-forgot-password.dto.js +59 -0
  60. package/dist/dto/confirm-forgot-password.dto.js.map +1 -1
  61. package/dist/dto/error-response.dto.d.ts +103 -0
  62. package/dist/dto/error-response.dto.d.ts.map +1 -1
  63. package/dist/dto/error-response.dto.js +103 -0
  64. package/dist/dto/error-response.dto.js.map +1 -1
  65. package/dist/dto/forgot-password.dto.d.ts +58 -0
  66. package/dist/dto/forgot-password.dto.d.ts.map +1 -1
  67. package/dist/dto/forgot-password.dto.js +58 -0
  68. package/dist/dto/forgot-password.dto.js.map +1 -1
  69. package/dist/dto/get-available-methods.dto.d.ts +37 -0
  70. package/dist/dto/get-available-methods.dto.d.ts.map +1 -1
  71. package/dist/dto/get-available-methods.dto.js +37 -0
  72. package/dist/dto/get-available-methods.dto.js.map +1 -1
  73. package/dist/dto/get-challenge-data-response.dto.d.ts +24 -0
  74. package/dist/dto/get-challenge-data-response.dto.d.ts.map +1 -1
  75. package/dist/dto/get-challenge-data-response.dto.js +24 -0
  76. package/dist/dto/get-challenge-data-response.dto.js.map +1 -1
  77. package/dist/dto/get-challenge-data.dto.d.ts +46 -0
  78. package/dist/dto/get-challenge-data.dto.d.ts.map +1 -1
  79. package/dist/dto/get-challenge-data.dto.js +46 -0
  80. package/dist/dto/get-challenge-data.dto.js.map +1 -1
  81. package/dist/dto/get-client-info.dto.d.ts +74 -0
  82. package/dist/dto/get-client-info.dto.d.ts.map +1 -1
  83. package/dist/dto/get-client-info.dto.js +74 -0
  84. package/dist/dto/get-client-info.dto.js.map +1 -1
  85. package/dist/dto/get-device-token-response.dto.d.ts +21 -0
  86. package/dist/dto/get-device-token-response.dto.d.ts.map +1 -1
  87. package/dist/dto/get-device-token-response.dto.js +21 -0
  88. package/dist/dto/get-device-token-response.dto.js.map +1 -1
  89. package/dist/dto/get-events-by-type.dto.d.ts +50 -0
  90. package/dist/dto/get-events-by-type.dto.d.ts.map +1 -1
  91. package/dist/dto/get-events-by-type.dto.js +50 -0
  92. package/dist/dto/get-events-by-type.dto.js.map +1 -1
  93. package/dist/dto/get-ip-address-response.dto.d.ts +20 -0
  94. package/dist/dto/get-ip-address-response.dto.d.ts.map +1 -1
  95. package/dist/dto/get-ip-address-response.dto.js +20 -0
  96. package/dist/dto/get-ip-address-response.dto.js.map +1 -1
  97. package/dist/dto/get-mfa-status.dto.d.ts +59 -0
  98. package/dist/dto/get-mfa-status.dto.d.ts.map +1 -1
  99. package/dist/dto/get-mfa-status.dto.js +59 -0
  100. package/dist/dto/get-mfa-status.dto.js.map +1 -1
  101. package/dist/dto/get-risk-assessment-history.dto.d.ts +28 -0
  102. package/dist/dto/get-risk-assessment-history.dto.d.ts.map +1 -1
  103. package/dist/dto/get-risk-assessment-history.dto.js +28 -0
  104. package/dist/dto/get-risk-assessment-history.dto.js.map +1 -1
  105. package/dist/dto/get-session-id-response.dto.d.ts +21 -0
  106. package/dist/dto/get-session-id-response.dto.d.ts.map +1 -1
  107. package/dist/dto/get-session-id-response.dto.js +21 -0
  108. package/dist/dto/get-session-id-response.dto.js.map +1 -1
  109. package/dist/dto/get-setup-data-response.dto.d.ts +27 -0
  110. package/dist/dto/get-setup-data-response.dto.d.ts.map +1 -1
  111. package/dist/dto/get-setup-data-response.dto.js +27 -0
  112. package/dist/dto/get-setup-data-response.dto.js.map +1 -1
  113. package/dist/dto/get-setup-data.dto.d.ts +51 -0
  114. package/dist/dto/get-setup-data.dto.d.ts.map +1 -1
  115. package/dist/dto/get-setup-data.dto.js +51 -0
  116. package/dist/dto/get-setup-data.dto.js.map +1 -1
  117. package/dist/dto/get-suspicious-activity.dto.d.ts +31 -0
  118. package/dist/dto/get-suspicious-activity.dto.d.ts.map +1 -1
  119. package/dist/dto/get-suspicious-activity.dto.js +31 -0
  120. package/dist/dto/get-suspicious-activity.dto.js.map +1 -1
  121. package/dist/dto/get-user-agent-response.dto.d.ts +19 -0
  122. package/dist/dto/get-user-agent-response.dto.d.ts.map +1 -1
  123. package/dist/dto/get-user-agent-response.dto.js +19 -0
  124. package/dist/dto/get-user-agent-response.dto.js.map +1 -1
  125. package/dist/dto/get-user-auth-history.dto.d.ts +64 -0
  126. package/dist/dto/get-user-auth-history.dto.d.ts.map +1 -1
  127. package/dist/dto/get-user-auth-history.dto.js +64 -0
  128. package/dist/dto/get-user-auth-history.dto.js.map +1 -1
  129. package/dist/dto/get-user-by-email.dto.d.ts +42 -0
  130. package/dist/dto/get-user-by-email.dto.d.ts.map +1 -1
  131. package/dist/dto/get-user-by-email.dto.js +42 -0
  132. package/dist/dto/get-user-by-email.dto.js.map +1 -1
  133. package/dist/dto/get-user-by-id.dto.d.ts +32 -0
  134. package/dist/dto/get-user-by-id.dto.d.ts.map +1 -1
  135. package/dist/dto/get-user-by-id.dto.js +32 -0
  136. package/dist/dto/get-user-by-id.dto.js.map +1 -1
  137. package/dist/dto/get-user-devices.dto.d.ts +34 -0
  138. package/dist/dto/get-user-devices.dto.d.ts.map +1 -1
  139. package/dist/dto/get-user-devices.dto.js +34 -0
  140. package/dist/dto/get-user-devices.dto.js.map +1 -1
  141. package/dist/dto/get-user-response.dto.d.ts +14 -0
  142. package/dist/dto/get-user-response.dto.d.ts.map +1 -1
  143. package/dist/dto/get-user-response.dto.js +15 -0
  144. package/dist/dto/get-user-response.dto.js.map +1 -1
  145. package/dist/dto/has-provider.dto.d.ts +33 -0
  146. package/dist/dto/has-provider.dto.d.ts.map +1 -1
  147. package/dist/dto/has-provider.dto.js +33 -0
  148. package/dist/dto/has-provider.dto.js.map +1 -1
  149. package/dist/dto/index.js +5 -0
  150. package/dist/dto/index.js.map +1 -1
  151. package/dist/dto/is-trusted-device-response.dto.d.ts +28 -0
  152. package/dist/dto/is-trusted-device-response.dto.d.ts.map +1 -1
  153. package/dist/dto/is-trusted-device-response.dto.js +28 -0
  154. package/dist/dto/is-trusted-device-response.dto.js.map +1 -1
  155. package/dist/dto/list-providers-response.dto.d.ts +19 -0
  156. package/dist/dto/list-providers-response.dto.d.ts.map +1 -1
  157. package/dist/dto/list-providers-response.dto.js +19 -0
  158. package/dist/dto/list-providers-response.dto.js.map +1 -1
  159. package/dist/dto/login.dto.d.ts +48 -0
  160. package/dist/dto/login.dto.d.ts.map +1 -1
  161. package/dist/dto/login.dto.js +50 -1
  162. package/dist/dto/login.dto.js.map +1 -1
  163. package/dist/dto/logout-all-response.dto.d.ts +20 -0
  164. package/dist/dto/logout-all-response.dto.d.ts.map +1 -1
  165. package/dist/dto/logout-all-response.dto.js +20 -0
  166. package/dist/dto/logout-all-response.dto.js.map +1 -1
  167. package/dist/dto/logout-all.dto.d.ts +42 -0
  168. package/dist/dto/logout-all.dto.d.ts.map +1 -1
  169. package/dist/dto/logout-all.dto.js +42 -0
  170. package/dist/dto/logout-all.dto.js.map +1 -1
  171. package/dist/dto/logout-response.dto.d.ts +21 -0
  172. package/dist/dto/logout-response.dto.d.ts.map +1 -1
  173. package/dist/dto/logout-response.dto.js +21 -0
  174. package/dist/dto/logout-response.dto.js.map +1 -1
  175. package/dist/dto/logout.dto.d.ts +45 -0
  176. package/dist/dto/logout.dto.d.ts.map +1 -1
  177. package/dist/dto/logout.dto.js +45 -0
  178. package/dist/dto/logout.dto.js.map +1 -1
  179. package/dist/dto/refresh-token.dto.d.ts +28 -0
  180. package/dist/dto/refresh-token.dto.d.ts.map +1 -1
  181. package/dist/dto/refresh-token.dto.js +28 -0
  182. package/dist/dto/refresh-token.dto.js.map +1 -1
  183. package/dist/dto/remove-devices.dto.d.ts +51 -0
  184. package/dist/dto/remove-devices.dto.d.ts.map +1 -1
  185. package/dist/dto/remove-devices.dto.js +51 -0
  186. package/dist/dto/remove-devices.dto.js.map +1 -1
  187. package/dist/dto/resend-code-response.dto.d.ts +28 -0
  188. package/dist/dto/resend-code-response.dto.d.ts.map +1 -1
  189. package/dist/dto/resend-code-response.dto.js +28 -0
  190. package/dist/dto/resend-code-response.dto.js.map +1 -1
  191. package/dist/dto/resend-code.dto.d.ts +37 -0
  192. package/dist/dto/resend-code.dto.d.ts.map +1 -1
  193. package/dist/dto/resend-code.dto.js +37 -0
  194. package/dist/dto/resend-code.dto.js.map +1 -1
  195. package/dist/dto/reset-password.dto.d.ts +74 -0
  196. package/dist/dto/reset-password.dto.d.ts.map +1 -1
  197. package/dist/dto/reset-password.dto.js +76 -1
  198. package/dist/dto/reset-password.dto.js.map +1 -1
  199. package/dist/dto/respond-challenge.dto.d.ts +147 -0
  200. package/dist/dto/respond-challenge.dto.d.ts.map +1 -1
  201. package/dist/dto/respond-challenge.dto.js +162 -0
  202. package/dist/dto/respond-challenge.dto.js.map +1 -1
  203. package/dist/dto/set-mfa-exemption.dto.d.ts +65 -0
  204. package/dist/dto/set-mfa-exemption.dto.d.ts.map +1 -1
  205. package/dist/dto/set-mfa-exemption.dto.js +65 -0
  206. package/dist/dto/set-mfa-exemption.dto.js.map +1 -1
  207. package/dist/dto/set-must-change-password-response.dto.d.ts +23 -0
  208. package/dist/dto/set-must-change-password-response.dto.d.ts.map +1 -1
  209. package/dist/dto/set-must-change-password-response.dto.js +23 -0
  210. package/dist/dto/set-must-change-password-response.dto.js.map +1 -1
  211. package/dist/dto/set-must-change-password.dto.d.ts +32 -0
  212. package/dist/dto/set-must-change-password.dto.d.ts.map +1 -1
  213. package/dist/dto/set-must-change-password.dto.js +32 -0
  214. package/dist/dto/set-must-change-password.dto.js.map +1 -1
  215. package/dist/dto/set-preferred-method.dto.d.ts +48 -0
  216. package/dist/dto/set-preferred-method.dto.d.ts.map +1 -1
  217. package/dist/dto/set-preferred-method.dto.js +48 -0
  218. package/dist/dto/set-preferred-method.dto.js.map +1 -1
  219. package/dist/dto/setup-mfa.dto.d.ts +62 -0
  220. package/dist/dto/setup-mfa.dto.d.ts.map +1 -1
  221. package/dist/dto/setup-mfa.dto.js +62 -0
  222. package/dist/dto/setup-mfa.dto.js.map +1 -1
  223. package/dist/dto/signup.dto.d.ts +92 -0
  224. package/dist/dto/signup.dto.d.ts.map +1 -1
  225. package/dist/dto/signup.dto.js +93 -0
  226. package/dist/dto/signup.dto.js.map +1 -1
  227. package/dist/dto/social-auth.dto.d.ts +234 -0
  228. package/dist/dto/social-auth.dto.d.ts.map +1 -1
  229. package/dist/dto/social-auth.dto.js +234 -0
  230. package/dist/dto/social-auth.dto.js.map +1 -1
  231. package/dist/dto/trust-device-response.dto.d.ts +26 -0
  232. package/dist/dto/trust-device-response.dto.d.ts.map +1 -1
  233. package/dist/dto/trust-device-response.dto.js +26 -0
  234. package/dist/dto/trust-device-response.dto.js.map +1 -1
  235. package/dist/dto/trust-device.dto.d.ts +9 -0
  236. package/dist/dto/trust-device.dto.d.ts.map +1 -1
  237. package/dist/dto/trust-device.dto.js +9 -0
  238. package/dist/dto/trust-device.dto.js.map +1 -1
  239. package/dist/dto/update-user-attributes-request.dto.d.ts +36 -0
  240. package/dist/dto/update-user-attributes-request.dto.d.ts.map +1 -1
  241. package/dist/dto/update-user-attributes-request.dto.js +36 -0
  242. package/dist/dto/update-user-attributes-request.dto.js.map +1 -1
  243. package/dist/dto/user-response.dto.d.ts +81 -0
  244. package/dist/dto/user-response.dto.d.ts.map +1 -1
  245. package/dist/dto/user-response.dto.js +84 -2
  246. package/dist/dto/user-response.dto.js.map +1 -1
  247. package/dist/dto/user-update.dto.d.ts +132 -0
  248. package/dist/dto/user-update.dto.d.ts.map +1 -1
  249. package/dist/dto/user-update.dto.js +133 -0
  250. package/dist/dto/user-update.dto.js.map +1 -1
  251. package/dist/dto/verify-email.dto.d.ts +171 -0
  252. package/dist/dto/verify-email.dto.d.ts.map +1 -1
  253. package/dist/dto/verify-email.dto.js +173 -1
  254. package/dist/dto/verify-email.dto.js.map +1 -1
  255. package/dist/dto/verify-mfa-code.dto.d.ts +65 -0
  256. package/dist/dto/verify-mfa-code.dto.d.ts.map +1 -1
  257. package/dist/dto/verify-mfa-code.dto.js +65 -0
  258. package/dist/dto/verify-mfa-code.dto.js.map +1 -1
  259. package/dist/dto/verify-phone-by-sub.dto.d.ts +49 -0
  260. package/dist/dto/verify-phone-by-sub.dto.d.ts.map +1 -1
  261. package/dist/dto/verify-phone-by-sub.dto.js +49 -0
  262. package/dist/dto/verify-phone-by-sub.dto.js.map +1 -1
  263. package/dist/dto/verify-phone.dto.d.ts +139 -0
  264. package/dist/dto/verify-phone.dto.d.ts.map +1 -1
  265. package/dist/dto/verify-phone.dto.js +142 -1
  266. package/dist/dto/verify-phone.dto.js.map +1 -1
  267. package/dist/dto.d.ts +10 -0
  268. package/dist/dto.d.ts.map +1 -1
  269. package/dist/dto.js +10 -0
  270. package/dist/dto.js.map +1 -1
  271. package/dist/entities/auth-audit.entity.d.ts +159 -0
  272. package/dist/entities/auth-audit.entity.d.ts.map +1 -1
  273. package/dist/entities/auth-audit.entity.js +166 -0
  274. package/dist/entities/auth-audit.entity.js.map +1 -1
  275. package/dist/entities/challenge-session.entity.d.ts +87 -0
  276. package/dist/entities/challenge-session.entity.d.ts.map +1 -1
  277. package/dist/entities/challenge-session.entity.js +87 -0
  278. package/dist/entities/challenge-session.entity.js.map +1 -1
  279. package/dist/entities/index.d.ts +18 -0
  280. package/dist/entities/index.d.ts.map +1 -1
  281. package/dist/entities/index.js +18 -0
  282. package/dist/entities/index.js.map +1 -1
  283. package/dist/entities/login-attempt.entity.d.ts +43 -0
  284. package/dist/entities/login-attempt.entity.d.ts.map +1 -1
  285. package/dist/entities/login-attempt.entity.js +43 -0
  286. package/dist/entities/login-attempt.entity.js.map +1 -1
  287. package/dist/entities/mfa-device.entity.d.ts +112 -0
  288. package/dist/entities/mfa-device.entity.d.ts.map +1 -1
  289. package/dist/entities/mfa-device.entity.js +112 -0
  290. package/dist/entities/mfa-device.entity.js.map +1 -1
  291. package/dist/entities/rate-limit.entity.d.ts +31 -0
  292. package/dist/entities/rate-limit.entity.d.ts.map +1 -1
  293. package/dist/entities/rate-limit.entity.js +31 -0
  294. package/dist/entities/rate-limit.entity.js.map +1 -1
  295. package/dist/entities/session.entity.d.ts +121 -0
  296. package/dist/entities/session.entity.d.ts.map +1 -1
  297. package/dist/entities/session.entity.js +121 -0
  298. package/dist/entities/session.entity.js.map +1 -1
  299. package/dist/entities/social-account.entity.d.ts +75 -0
  300. package/dist/entities/social-account.entity.d.ts.map +1 -1
  301. package/dist/entities/social-account.entity.js +75 -0
  302. package/dist/entities/social-account.entity.js.map +1 -1
  303. package/dist/entities/storage-lock.entity.d.ts +28 -0
  304. package/dist/entities/storage-lock.entity.d.ts.map +1 -1
  305. package/dist/entities/storage-lock.entity.js +28 -0
  306. package/dist/entities/storage-lock.entity.js.map +1 -1
  307. package/dist/entities/trusted-device.entity.d.ts +83 -0
  308. package/dist/entities/trusted-device.entity.d.ts.map +1 -1
  309. package/dist/entities/trusted-device.entity.js +83 -0
  310. package/dist/entities/trusted-device.entity.js.map +1 -1
  311. package/dist/entities/user.entity.d.ts +166 -0
  312. package/dist/entities/user.entity.d.ts.map +1 -1
  313. package/dist/entities/user.entity.js +166 -0
  314. package/dist/entities/user.entity.js.map +1 -1
  315. package/dist/entities/verification-token.entity.d.ts +102 -0
  316. package/dist/entities/verification-token.entity.d.ts.map +1 -1
  317. package/dist/entities/verification-token.entity.js +102 -0
  318. package/dist/entities/verification-token.entity.js.map +1 -1
  319. package/dist/entities.d.ts +8 -0
  320. package/dist/entities.d.ts.map +1 -1
  321. package/dist/entities.js +8 -0
  322. package/dist/entities.js.map +1 -1
  323. package/dist/enums/auth-audit-event-type.enum.d.ts +211 -0
  324. package/dist/enums/auth-audit-event-type.enum.d.ts.map +1 -1
  325. package/dist/enums/auth-audit-event-type.enum.js +244 -0
  326. package/dist/enums/auth-audit-event-type.enum.js.map +1 -1
  327. package/dist/enums/error-codes.enum.d.ts +296 -0
  328. package/dist/enums/error-codes.enum.d.ts.map +1 -1
  329. package/dist/enums/error-codes.enum.js +332 -0
  330. package/dist/enums/error-codes.enum.js.map +1 -1
  331. package/dist/enums/mfa-method.enum.d.ts +74 -0
  332. package/dist/enums/mfa-method.enum.d.ts.map +1 -1
  333. package/dist/enums/mfa-method.enum.js +64 -0
  334. package/dist/enums/mfa-method.enum.js.map +1 -1
  335. package/dist/enums/risk-factor.enum.d.ts +91 -0
  336. package/dist/enums/risk-factor.enum.d.ts.map +1 -1
  337. package/dist/enums/risk-factor.enum.js +97 -0
  338. package/dist/enums/risk-factor.enum.js.map +1 -1
  339. package/dist/exceptions/nauth.exception.d.ts +149 -0
  340. package/dist/exceptions/nauth.exception.d.ts.map +1 -1
  341. package/dist/exceptions/nauth.exception.js +159 -0
  342. package/dist/exceptions/nauth.exception.js.map +1 -1
  343. package/dist/handlers/auth.handler.d.ts +32 -0
  344. package/dist/handlers/auth.handler.d.ts.map +1 -1
  345. package/dist/handlers/auth.handler.js +47 -1
  346. package/dist/handlers/auth.handler.js.map +1 -1
  347. package/dist/handlers/client-info.handler.d.ts +25 -0
  348. package/dist/handlers/client-info.handler.d.ts.map +1 -1
  349. package/dist/handlers/client-info.handler.js +36 -2
  350. package/dist/handlers/client-info.handler.js.map +1 -1
  351. package/dist/handlers/csrf.handler.d.ts +32 -0
  352. package/dist/handlers/csrf.handler.d.ts.map +1 -1
  353. package/dist/handlers/csrf.handler.js +49 -1
  354. package/dist/handlers/csrf.handler.js.map +1 -1
  355. package/dist/handlers/token-delivery.handler.d.ts +16 -0
  356. package/dist/handlers/token-delivery.handler.d.ts.map +1 -1
  357. package/dist/handlers/token-delivery.handler.js +22 -1
  358. package/dist/handlers/token-delivery.handler.js.map +1 -1
  359. package/dist/index.d.ts +34 -0
  360. package/dist/index.d.ts.map +1 -1
  361. package/dist/index.js +67 -0
  362. package/dist/index.js.map +1 -1
  363. package/dist/interfaces/client-info.interface.d.ts +58 -0
  364. package/dist/interfaces/client-info.interface.d.ts.map +1 -1
  365. package/dist/interfaces/config.interface.d.ts +1774 -0
  366. package/dist/interfaces/config.interface.d.ts.map +1 -1
  367. package/dist/interfaces/config.interface.js +16 -0
  368. package/dist/interfaces/config.interface.js.map +1 -1
  369. package/dist/interfaces/entities.interface.d.ts +48 -0
  370. package/dist/interfaces/entities.interface.d.ts.map +1 -1
  371. package/dist/interfaces/entities.interface.js +8 -0
  372. package/dist/interfaces/entities.interface.js.map +1 -1
  373. package/dist/interfaces/index.js +5 -0
  374. package/dist/interfaces/index.js.map +1 -1
  375. package/dist/interfaces/logger.interface.d.ts +213 -0
  376. package/dist/interfaces/logger.interface.d.ts.map +1 -1
  377. package/dist/interfaces/logger.interface.js +35 -0
  378. package/dist/interfaces/logger.interface.js.map +1 -1
  379. package/dist/interfaces/mfa-provider.interface.d.ts +134 -0
  380. package/dist/interfaces/mfa-provider.interface.d.ts.map +1 -1
  381. package/dist/interfaces/oauth.interface.d.ts +110 -0
  382. package/dist/interfaces/oauth.interface.d.ts.map +1 -1
  383. package/dist/interfaces/provider.interface.d.ts +83 -0
  384. package/dist/interfaces/provider.interface.d.ts.map +1 -1
  385. package/dist/interfaces/sms-template.interface.d.ts +246 -0
  386. package/dist/interfaces/sms-template.interface.d.ts.map +1 -1
  387. package/dist/interfaces/sms-template.interface.js +26 -0
  388. package/dist/interfaces/sms-template.interface.js.map +1 -1
  389. package/dist/interfaces/social-auth-provider.interface.d.ts +115 -0
  390. package/dist/interfaces/social-auth-provider.interface.d.ts.map +1 -1
  391. package/dist/interfaces/storage-adapter.interface.d.ts +37 -0
  392. package/dist/interfaces/storage-adapter.interface.d.ts.map +1 -1
  393. package/dist/interfaces/template.interface.d.ts +351 -0
  394. package/dist/interfaces/template.interface.d.ts.map +1 -1
  395. package/dist/interfaces/template.interface.js +13 -0
  396. package/dist/interfaces/template.interface.js.map +1 -1
  397. package/dist/interfaces/token-verifier.interface.d.ts +101 -0
  398. package/dist/interfaces/token-verifier.interface.d.ts.map +1 -1
  399. package/dist/interfaces.d.ts +8 -0
  400. package/dist/interfaces.d.ts.map +1 -1
  401. package/dist/interfaces.js +8 -0
  402. package/dist/interfaces.js.map +1 -1
  403. package/dist/internal.d.ts +120 -0
  404. package/dist/internal.d.ts.map +1 -1
  405. package/dist/internal.js +138 -0
  406. package/dist/internal.js.map +1 -1
  407. package/dist/platform/interfaces.d.ts +187 -0
  408. package/dist/platform/interfaces.d.ts.map +1 -1
  409. package/dist/platform/interfaces.js +11 -0
  410. package/dist/platform/interfaces.js.map +1 -1
  411. package/dist/schemas/auth-config.schema.d.ts +48 -0
  412. package/dist/schemas/auth-config.schema.d.ts.map +1 -1
  413. package/dist/schemas/auth-config.schema.js +188 -9
  414. package/dist/schemas/auth-config.schema.js.map +1 -1
  415. package/dist/services/adaptive-mfa-decision.service.d.ts +144 -0
  416. package/dist/services/adaptive-mfa-decision.service.d.ts.map +1 -1
  417. package/dist/services/adaptive-mfa-decision.service.js +151 -5
  418. package/dist/services/adaptive-mfa-decision.service.js.map +1 -1
  419. package/dist/services/auth-audit.service.d.ts +195 -0
  420. package/dist/services/auth-audit.service.d.ts.map +1 -1
  421. package/dist/services/auth-audit.service.js +228 -1
  422. package/dist/services/auth-audit.service.js.map +1 -1
  423. package/dist/services/auth-challenge-helper.service.d.ts +144 -1
  424. package/dist/services/auth-challenge-helper.service.d.ts.map +1 -1
  425. package/dist/services/auth-challenge-helper.service.js +295 -16
  426. package/dist/services/auth-challenge-helper.service.js.map +1 -1
  427. package/dist/services/auth-flow-context-builder.service.d.ts +120 -1
  428. package/dist/services/auth-flow-context-builder.service.d.ts.map +1 -1
  429. package/dist/services/auth-flow-context-builder.service.js +184 -5
  430. package/dist/services/auth-flow-context-builder.service.js.map +1 -1
  431. package/dist/services/auth-flow-rules.d.ts +136 -0
  432. package/dist/services/auth-flow-rules.d.ts.map +1 -1
  433. package/dist/services/auth-flow-rules.js +137 -0
  434. package/dist/services/auth-flow-rules.js.map +1 -1
  435. package/dist/services/auth-flow-state-definitions.d.ts +40 -0
  436. package/dist/services/auth-flow-state-definitions.d.ts.map +1 -1
  437. package/dist/services/auth-flow-state-definitions.js +98 -0
  438. package/dist/services/auth-flow-state-definitions.js.map +1 -1
  439. package/dist/services/auth-flow-state-machine.service.d.ts +91 -0
  440. package/dist/services/auth-flow-state-machine.service.d.ts.map +1 -1
  441. package/dist/services/auth-flow-state-machine.service.js +102 -0
  442. package/dist/services/auth-flow-state-machine.service.js.map +1 -1
  443. package/dist/services/auth-flow-state-machine.types.d.ts +221 -0
  444. package/dist/services/auth-flow-state-machine.types.d.ts.map +1 -1
  445. package/dist/services/auth-flow-state-machine.types.js +47 -0
  446. package/dist/services/auth-flow-state-machine.types.js.map +1 -1
  447. package/dist/services/auth.service.d.ts +397 -1
  448. package/dist/services/auth.service.d.ts.map +1 -1
  449. package/dist/services/auth.service.js +943 -27
  450. package/dist/services/auth.service.js.map +1 -1
  451. package/dist/services/challenge.service.d.ts +255 -1
  452. package/dist/services/challenge.service.d.ts.map +1 -1
  453. package/dist/services/challenge.service.js +327 -3
  454. package/dist/services/challenge.service.js.map +1 -1
  455. package/dist/services/client-info.service.d.ts +143 -0
  456. package/dist/services/client-info.service.d.ts.map +1 -1
  457. package/dist/services/client-info.service.js +161 -0
  458. package/dist/services/client-info.service.js.map +1 -1
  459. package/dist/services/csrf.service.d.ts +15 -0
  460. package/dist/services/csrf.service.d.ts.map +1 -1
  461. package/dist/services/csrf.service.js +16 -0
  462. package/dist/services/csrf.service.js.map +1 -1
  463. package/dist/services/email-verification.service.d.ts +52 -0
  464. package/dist/services/email-verification.service.d.ts.map +1 -1
  465. package/dist/services/email-verification.service.js +149 -10
  466. package/dist/services/email-verification.service.js.map +1 -1
  467. package/dist/services/geo-location.service.d.ts +105 -0
  468. package/dist/services/geo-location.service.d.ts.map +1 -1
  469. package/dist/services/geo-location.service.js +188 -2
  470. package/dist/services/geo-location.service.js.map +1 -1
  471. package/dist/services/jwt.service.d.ts +257 -0
  472. package/dist/services/jwt.service.d.ts.map +1 -1
  473. package/dist/services/jwt.service.js +284 -1
  474. package/dist/services/jwt.service.js.map +1 -1
  475. package/dist/services/mfa-base.service.d.ts +179 -1
  476. package/dist/services/mfa-base.service.d.ts.map +1 -1
  477. package/dist/services/mfa-base.service.js +256 -2
  478. package/dist/services/mfa-base.service.js.map +1 -1
  479. package/dist/services/mfa.service.d.ts +304 -0
  480. package/dist/services/mfa.service.d.ts.map +1 -1
  481. package/dist/services/mfa.service.js +380 -0
  482. package/dist/services/mfa.service.js.map +1 -1
  483. package/dist/services/password-reset.service.d.ts +46 -0
  484. package/dist/services/password-reset.service.d.ts.map +1 -1
  485. package/dist/services/password-reset.service.js +79 -0
  486. package/dist/services/password-reset.service.js.map +1 -1
  487. package/dist/services/password.service.d.ts +139 -0
  488. package/dist/services/password.service.d.ts.map +1 -1
  489. package/dist/services/password.service.js +167 -9
  490. package/dist/services/password.service.js.map +1 -1
  491. package/dist/services/phone-verification.service.d.ts +75 -0
  492. package/dist/services/phone-verification.service.d.ts.map +1 -1
  493. package/dist/services/phone-verification.service.js +188 -6
  494. package/dist/services/phone-verification.service.js.map +1 -1
  495. package/dist/services/risk-detection.service.d.ts +198 -0
  496. package/dist/services/risk-detection.service.d.ts.map +1 -1
  497. package/dist/services/risk-detection.service.js +358 -11
  498. package/dist/services/risk-detection.service.js.map +1 -1
  499. package/dist/services/risk-scoring.service.d.ts +84 -0
  500. package/dist/services/risk-scoring.service.d.ts.map +1 -1
  501. package/dist/services/risk-scoring.service.js +87 -0
  502. package/dist/services/risk-scoring.service.js.map +1 -1
  503. package/dist/services/session.service.d.ts +204 -0
  504. package/dist/services/session.service.d.ts.map +1 -1
  505. package/dist/services/session.service.js +289 -4
  506. package/dist/services/session.service.js.map +1 -1
  507. package/dist/services/social-auth-base.service.d.ts +123 -1
  508. package/dist/services/social-auth-base.service.d.ts.map +1 -1
  509. package/dist/services/social-auth-base.service.js +155 -2
  510. package/dist/services/social-auth-base.service.js.map +1 -1
  511. package/dist/services/social-auth.service.d.ts +191 -0
  512. package/dist/services/social-auth.service.d.ts.map +1 -1
  513. package/dist/services/social-auth.service.js +215 -2
  514. package/dist/services/social-auth.service.js.map +1 -1
  515. package/dist/services/social-provider-registry.service.d.ts +86 -0
  516. package/dist/services/social-provider-registry.service.d.ts.map +1 -1
  517. package/dist/services/social-provider-registry.service.js +86 -0
  518. package/dist/services/social-provider-registry.service.js.map +1 -1
  519. package/dist/services/trusted-device.service.d.ts +105 -0
  520. package/dist/services/trusted-device.service.d.ts.map +1 -1
  521. package/dist/services/trusted-device.service.js +133 -4
  522. package/dist/services/trusted-device.service.js.map +1 -1
  523. package/dist/storage/account-lockout-storage.service.d.ts +35 -0
  524. package/dist/storage/account-lockout-storage.service.d.ts.map +1 -1
  525. package/dist/storage/account-lockout-storage.service.js +35 -0
  526. package/dist/storage/account-lockout-storage.service.js.map +1 -1
  527. package/dist/storage/memory-storage.adapter.d.ts +148 -0
  528. package/dist/storage/memory-storage.adapter.d.ts.map +1 -1
  529. package/dist/storage/memory-storage.adapter.js +201 -6
  530. package/dist/storage/memory-storage.adapter.js.map +1 -1
  531. package/dist/storage/rate-limit-storage.service.d.ts +3 -0
  532. package/dist/storage/rate-limit-storage.service.d.ts.map +1 -1
  533. package/dist/storage/rate-limit-storage.service.js +4 -0
  534. package/dist/storage/rate-limit-storage.service.js.map +1 -1
  535. package/dist/storage.d.ts +8 -0
  536. package/dist/storage.d.ts.map +1 -1
  537. package/dist/storage.js +8 -0
  538. package/dist/storage.js.map +1 -1
  539. package/dist/templates/html-template.engine.d.ts +110 -0
  540. package/dist/templates/html-template.engine.d.ts.map +1 -1
  541. package/dist/templates/html-template.engine.js +147 -0
  542. package/dist/templates/html-template.engine.js.map +1 -1
  543. package/dist/templates/index.d.ts +5 -0
  544. package/dist/templates/index.d.ts.map +1 -1
  545. package/dist/templates/index.js +5 -0
  546. package/dist/templates/index.js.map +1 -1
  547. package/dist/templates/sms-template.engine.d.ts +151 -0
  548. package/dist/templates/sms-template.engine.d.ts.map +1 -1
  549. package/dist/templates/sms-template.engine.js +171 -0
  550. package/dist/templates/sms-template.engine.js.map +1 -1
  551. package/dist/templates.d.ts +8 -0
  552. package/dist/templates.d.ts.map +1 -1
  553. package/dist/templates.js +8 -0
  554. package/dist/templates.js.map +1 -1
  555. package/dist/utils/common-passwords.d.ts +42 -0
  556. package/dist/utils/common-passwords.d.ts.map +1 -1
  557. package/dist/utils/common-passwords.js +88 -0
  558. package/dist/utils/common-passwords.js.map +1 -1
  559. package/dist/utils/context-storage.d.ts +129 -0
  560. package/dist/utils/context-storage.d.ts.map +1 -1
  561. package/dist/utils/context-storage.js +129 -0
  562. package/dist/utils/context-storage.js.map +1 -1
  563. package/dist/utils/cookie-names.util.d.ts +35 -0
  564. package/dist/utils/cookie-names.util.d.ts.map +1 -1
  565. package/dist/utils/cookie-names.util.js +37 -0
  566. package/dist/utils/cookie-names.util.js.map +1 -1
  567. package/dist/utils/cookies.util.d.ts +19 -0
  568. package/dist/utils/cookies.util.d.ts.map +1 -1
  569. package/dist/utils/cookies.util.js +30 -3
  570. package/dist/utils/cookies.util.js.map +1 -1
  571. package/dist/utils/index.d.ts +3 -0
  572. package/dist/utils/index.d.ts.map +1 -1
  573. package/dist/utils/index.js +4 -0
  574. package/dist/utils/index.js.map +1 -1
  575. package/dist/utils/ip-extractor.d.ts +88 -0
  576. package/dist/utils/ip-extractor.d.ts.map +1 -1
  577. package/dist/utils/ip-extractor.js +109 -16
  578. package/dist/utils/ip-extractor.js.map +1 -1
  579. package/dist/utils/nauth-logger.d.ts +70 -0
  580. package/dist/utils/nauth-logger.d.ts.map +1 -1
  581. package/dist/utils/nauth-logger.js +82 -4
  582. package/dist/utils/nauth-logger.js.map +1 -1
  583. package/dist/utils/pii-redactor.d.ts +70 -0
  584. package/dist/utils/pii-redactor.d.ts.map +1 -1
  585. package/dist/utils/pii-redactor.js +102 -0
  586. package/dist/utils/pii-redactor.js.map +1 -1
  587. package/dist/utils/setup/get-repositories.d.ts +16 -0
  588. package/dist/utils/setup/get-repositories.d.ts.map +1 -1
  589. package/dist/utils/setup/get-repositories.js +21 -0
  590. package/dist/utils/setup/get-repositories.js.map +1 -1
  591. package/dist/utils/setup/init-services.d.ts +40 -1
  592. package/dist/utils/setup/init-services.d.ts.map +1 -1
  593. package/dist/utils/setup/init-services.js +98 -0
  594. package/dist/utils/setup/init-services.js.map +1 -1
  595. package/dist/utils/setup/init-social.d.ts +27 -0
  596. package/dist/utils/setup/init-social.d.ts.map +1 -1
  597. package/dist/utils/setup/init-social.js +49 -0
  598. package/dist/utils/setup/init-social.js.map +1 -1
  599. package/dist/utils/setup/init-storage.d.ts +22 -0
  600. package/dist/utils/setup/init-storage.d.ts.map +1 -1
  601. package/dist/utils/setup/init-storage.js +36 -0
  602. package/dist/utils/setup/init-storage.js.map +1 -1
  603. package/dist/utils/setup/register-mfa.d.ts +22 -0
  604. package/dist/utils/setup/register-mfa.d.ts.map +1 -1
  605. package/dist/utils/setup/register-mfa.js +41 -0
  606. package/dist/utils/setup/register-mfa.js.map +1 -1
  607. package/dist/utils/setup/run-nauth-migrations.d.ts +7 -0
  608. package/dist/utils/setup/run-nauth-migrations.d.ts.map +1 -1
  609. package/dist/utils/setup/run-nauth-migrations.js +8 -0
  610. package/dist/utils/setup/run-nauth-migrations.js.map +1 -1
  611. package/dist/utils/token-delivery-policy.d.ts +17 -0
  612. package/dist/utils/token-delivery-policy.d.ts.map +1 -1
  613. package/dist/utils/token-delivery-policy.js +17 -0
  614. package/dist/utils/token-delivery-policy.js.map +1 -1
  615. package/dist/utils.d.ts +8 -0
  616. package/dist/utils.d.ts.map +1 -1
  617. package/dist/utils.js +8 -0
  618. package/dist/utils.js.map +1 -1
  619. package/dist/validators/template.validator.d.ts +80 -0
  620. package/dist/validators/template.validator.d.ts.map +1 -1
  621. package/dist/validators/template.validator.js +94 -0
  622. package/dist/validators/template.validator.js.map +1 -1
  623. package/package.json +7 -2
@@ -47,6 +47,14 @@ const error_codes_enum_1 = require("../enums/error-codes.enum");
47
47
  const mfa_method_enum_1 = require("../enums/mfa-method.enum");
48
48
  const class_validator_1 = require("class-validator");
49
49
  const crypto = __importStar(require("crypto"));
50
+ /**
51
+ * Dummy Argon2 hash for constant-time response
52
+ *
53
+ * SECURITY CRITICAL: Used when user doesn't exist to prevent timing attacks
54
+ * This dummy hash has same format/cost as real Argon2id hashes but verifies against nothing.
55
+ *
56
+ * Format: $argon2id$v=19$m=65536,t=3,p=4$salt$hash
57
+ */
50
58
  const DUMMY_ARGON2_HASH = '$argon2id$v=19$m=65536,t=3,p=4$RFVNTVlfU0FMVF9GT1JfVElNSU5H$dummyhashfordummyhashfordummyhash1234567890';
51
59
  class AuthService {
52
60
  userRepository;
@@ -67,7 +75,12 @@ class AuthService {
67
75
  mfaDeviceRepository;
68
76
  trustedDeviceService;
69
77
  passwordResetService;
70
- constructor(userRepository, loginAttemptRepository, passwordService, jwtService, sessionService, challengeService, challengeHelper, emailVerificationService, clientInfoService, accountLockoutStorage, config, logger, auditService, phoneVerificationService, mfaService, mfaDeviceRepository, trustedDeviceService, passwordResetService) {
78
+ constructor(userRepository, loginAttemptRepository, passwordService, jwtService, sessionService, challengeService, challengeHelper, emailVerificationService, clientInfoService, accountLockoutStorage, config, logger, auditService, // Optional - audit trail service (enabled via config.auditLogs.enabled)
79
+ phoneVerificationService, // Optional - only available when SMS provider is configured
80
+ mfaService, // Optional - available when MFA modules are imported
81
+ mfaDeviceRepository, // Optional - available when MFA modules are imported
82
+ trustedDeviceService, // Optional - only available when rememberDevices is not 'never'
83
+ passwordResetService) {
71
84
  this.userRepository = userRepository;
72
85
  this.loginAttemptRepository = loginAttemptRepository;
73
86
  this.passwordService = passwordService;
@@ -88,14 +101,39 @@ class AuthService {
88
101
  this.passwordResetService = passwordResetService;
89
102
  this.logger?.log?.('AuthService initialized');
90
103
  }
104
+ // ============================================================================
105
+ // User Signup
106
+ // ============================================================================
107
+ /**
108
+ * Register a new user.
109
+ *
110
+ * Checks for duplicates (email, username, phone), validates password, hashes it,
111
+ * creates the user, and returns tokens or a challenge if verification is required.
112
+ *
113
+ * @param dto - Signup payload
114
+ * @returns Auth response with tokens or a verification challenge
115
+ * @throws {NAuthException} If user exists, password is invalid, or signup is disabled
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * const result = await authService.signup({
120
+ * email: 'user@example.com',
121
+ * password: 'Password123!',
122
+ * username: 'johndoe',
123
+ * });
124
+ * ```
125
+ */
91
126
  async signup(dto) {
127
+ // Get client info from request context (transparent!)
92
128
  const clientInfo = this.clientInfoService.get();
93
129
  this.logger?.log?.(`Signup attempt for email: ${dto.email}`);
94
130
  this.logger?.debug?.(`Signup details: { email: ${dto.email}, username: ${dto.username || 'none'}, ip: ${clientInfo.ipAddress} }`);
131
+ // Check if signup is enabled
95
132
  if (this.config.signup?.enabled === false) {
96
133
  this.logger?.warn?.(`Signup blocked - signup is disabled`);
97
134
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SIGNUP_DISABLED, 'Signups are currently disabled');
98
135
  }
136
+ // Check if user already exists (email and username)
99
137
  this.logger?.debug?.(`Checking if user exists: ${dto.email}`);
100
138
  const existingUserByEmail = await this.userRepository.findOne({
101
139
  where: { email: dto.email },
@@ -104,6 +142,7 @@ class AuthService {
104
142
  this.logger?.warn?.(`Signup failed - user already exists: ${dto.email}`);
105
143
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this email already exists');
106
144
  }
145
+ // Check for duplicate username if provided
107
146
  if (dto.username) {
108
147
  this.logger?.debug?.(`Checking if username exists: ${dto.username}`);
109
148
  const existingUserByUsername = await this.userRepository.findOne({
@@ -114,6 +153,7 @@ class AuthService {
114
153
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USERNAME_EXISTS, 'Username is already taken');
115
154
  }
116
155
  }
156
+ // Check for duplicate phone if provided and duplicates not allowed
117
157
  if (dto.phone && !this.config.signup?.allowDuplicatePhones) {
118
158
  this.logger?.debug?.(`Checking if phone exists: ${dto.phone}`);
119
159
  const existingUserByPhone = await this.userRepository.findOne({
@@ -124,6 +164,7 @@ class AuthService {
124
164
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PHONE_EXISTS, 'Phone number is already registered');
125
165
  }
126
166
  }
167
+ // Validate password policy
127
168
  this.logger?.debug?.('Validating password against policy');
128
169
  const passwordValidation = await this.passwordService.validatePassword(dto.password, {
129
170
  email: dto.email,
@@ -135,12 +176,19 @@ class AuthService {
135
176
  errors: passwordValidation.errors,
136
177
  });
137
178
  }
179
+ // Hash password
138
180
  const passwordHash = await this.passwordService.hashPassword(dto.password);
181
+ // Determine verification requirements based on verification method
139
182
  const verificationMethod = this.config.signup?.verificationMethod;
183
+ // Validate required fields based on verification method
140
184
  if ((verificationMethod === 'phone' || verificationMethod === 'both') && !dto.phone) {
141
185
  this.logger?.warn?.(`Signup failed - phone required for verification method: ${verificationMethod}`);
142
186
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PHONE_REQUIRED, 'Phone number is required for the selected verification method', { verificationMethod });
143
187
  }
188
+ // Create user
189
+ // Users are always created as ACTIVE (so they can complete pending challenges)
190
+ // Verification status controls access via challenge system, not account activation
191
+ // Email and phone verification status is always false initially - must be explicitly verified
144
192
  this.logger?.debug?.(`Creating user record for: ${dto.email} || ${dto.username} || ${dto.phone}`);
145
193
  const user = this.userRepository.create({
146
194
  email: dto.email,
@@ -150,21 +198,25 @@ class AuthService {
150
198
  phone: dto.phone,
151
199
  passwordHash,
152
200
  passwordChangedAt: new Date(),
153
- isEmailVerified: false,
154
- isPhoneVerified: false,
155
- isActive: true,
201
+ isEmailVerified: false, // Always false initially - must be explicitly verified
202
+ isPhoneVerified: false, // Always false initially - must be verified via SMS
203
+ isActive: true, // Always active - challenges control access instead
156
204
  metadata: dto.metadata,
157
205
  });
158
206
  let savedUser;
159
207
  try {
160
208
  savedUser = (await this.userRepository.save(user));
161
209
  this.logger?.log?.(`User created successfully: ${dto.email} (sub: ${savedUser.sub})`);
210
+ // ============================================================================
211
+ // Audit: Record account creation
212
+ // ============================================================================
162
213
  try {
163
214
  await this.auditService?.recordEvent({
164
215
  userId: savedUser.id,
165
216
  eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ACCOUNT_CREATED,
166
217
  eventStatus: 'INFO',
167
218
  authMethod: 'password',
219
+ // Client info automatically included from context
168
220
  metadata: {
169
221
  email: savedUser.email,
170
222
  username: savedUser.username || null,
@@ -173,6 +225,7 @@ class AuthService {
173
225
  });
174
226
  }
175
227
  catch (auditError) {
228
+ // Non-blocking: Log but continue
176
229
  const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
177
230
  this.logger?.error?.(`Failed to record ACCOUNT_CREATED audit event: ${errorMessage}`, {
178
231
  error: auditError,
@@ -181,7 +234,9 @@ class AuthService {
181
234
  }
182
235
  }
183
236
  catch (error) {
237
+ // Handle database constraint violations gracefully
184
238
  if (error && typeof error === 'object' && 'code' in error && error.code === '23505') {
239
+ // PostgreSQL unique constraint violation
185
240
  const dbError = error;
186
241
  if (dbError.detail?.includes('email')) {
187
242
  this.logger?.warn?.(`Signup failed - email constraint violation: ${dto.email}`);
@@ -202,13 +257,24 @@ class AuthService {
202
257
  });
203
258
  }
204
259
  }
260
+ // Re-throw other database errors
205
261
  const errorMessage = error instanceof Error ? error.message : 'Unknown database error';
206
262
  this.logger?.error?.(`Signup failed - database error: ${errorMessage}`);
207
263
  throw error;
208
264
  }
265
+ // ============================================================================
266
+ // Verification Code Sending: Handled by challenge system (sequential flow)
267
+ // ============================================================================
268
+ // All verification codes are sent when challenges are created (in AuthChallengeHelperService.createChallengeResponse)
269
+ // This ensures proper sequential flow: email code first, then phone code after email is verified
270
+ // This prevents user confusion from receiving multiple codes at once
271
+ // Execute afterSignup hook if configured
209
272
  if (this.config.hooks?.afterSignup) {
210
273
  await this.config.hooks.afterSignup(savedUser, { requiresVerification: verificationMethod !== 'none' });
211
274
  }
275
+ // ============================================================================
276
+ // Challenge System: Determine if user needs to complete challenges
277
+ // ============================================================================
212
278
  const response = await this.challengeHelper.determineAuthResponse({
213
279
  user: savedUser,
214
280
  config: this.config,
@@ -222,11 +288,34 @@ class AuthService {
222
288
  }
223
289
  return response;
224
290
  }
291
+ // ============================================================================
292
+ // User Login
293
+ // ============================================================================
294
+ /**
295
+ * Log in a user with identifier (email, username, or phone) and password.
296
+ *
297
+ * Handles client/device context, login hooks, lockout checks, audit logging, password verification,
298
+ * and challenge flow (MFA/verification) if required.
299
+ *
300
+ * @param dto - Login credentials (identifier and password)
301
+ * @returns Authentication response containing challenge details if required, or tokens on success
302
+ * @throws {NAuthException} On login failure, forbidden access, or account lockout
303
+ *
304
+ * @example
305
+ * ```typescript
306
+ * const res = await authService.login({ identifier: 'user@email.com', password: 'Pass123!' });
307
+ * if (res.challengeName) {
308
+ * // prompt user for verification code
309
+ * }
310
+ * ```
311
+ */
225
312
  async login(dto) {
313
+ // Get client info from request context (transparent!)
226
314
  const clientInfo = this.clientInfoService.get();
227
315
  const fireAndForget = this.config.auditLogs?.fireAndForget === true;
228
316
  this.logger?.log?.(`Login attempt for: ${dto.identifier}`);
229
317
  this.logger?.debug?.(`Login details: { identifier: ${dto.identifier}, ip: ${clientInfo.ipAddress}, deviceToken: ${clientInfo.deviceToken ? 'present' : 'none'} }`);
318
+ // Check IP-based account lockout
230
319
  if (this.config.lockout?.enabled) {
231
320
  const clientInfo = this.clientInfoService.get();
232
321
  const ipAddress = clientInfo.ipAddress;
@@ -236,6 +325,9 @@ class AuthService {
236
325
  if (isLocked) {
237
326
  this.logger?.warn?.(`Login blocked - IP locked: ${ipAddress}`);
238
327
  await this.recordLoginAttempt(dto.identifier, false, 'ip_locked');
328
+ // ============================================================================
329
+ // Audit: Record blocked login (IP locked)
330
+ // ============================================================================
239
331
  if (fireAndForget) {
240
332
  this.auditService
241
333
  ?.recordEvent({
@@ -276,6 +368,9 @@ class AuthService {
276
368
  }
277
369
  }
278
370
  }
371
+ // ============================================================================
372
+ // Validate identifier type based on configuration
373
+ // ============================================================================
279
374
  const identifierType = this.config.login?.identifierType;
280
375
  if (identifierType) {
281
376
  this.logger?.debug?.(`Validating identifier type for: ${dto.identifier}, allowed type: ${identifierType}`);
@@ -286,14 +381,22 @@ class AuthService {
286
381
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INVALID_CREDENTIALS, `Login with this identifier type is not allowed. Expected: ${identifierType}`);
287
382
  }
288
383
  }
384
+ // Find user by email, username, or phone (filtered by identifierType config)
289
385
  this.logger?.debug?.(`Finding user by identifier: ${dto.identifier}`);
290
386
  const user = await this.findUserByIdentifier(dto.identifier, identifierType);
387
+ // SECURITY CRITICAL: Always hash password even when user doesn't exist
388
+ // This ensures constant-time response to prevent user enumeration via timing attacks
291
389
  const hashToVerify = user?.passwordHash || DUMMY_ARGON2_HASH;
390
+ // Verify password (takes ~200-300ms regardless of user existence)
292
391
  this.logger?.debug?.('Verifying password');
293
392
  const isPasswordValid = await this.passwordService.verifyPassword(dto.password, hashToVerify);
393
+ // Now check all conditions AFTER password verification (constant time achieved)
294
394
  if (!user || !user.passwordHash || !isPasswordValid) {
295
395
  this.logger?.warn?.(`Login failed - invalid credentials for: ${dto.identifier}`);
296
396
  await this.handleFailedLogin(dto.identifier, 'invalid_credentials');
397
+ // ============================================================================
398
+ // Audit: Record failed login
399
+ // ============================================================================
297
400
  if (user) {
298
401
  if (fireAndForget) {
299
402
  this.auditService
@@ -334,6 +437,7 @@ class AuthService {
334
437
  }
335
438
  }
336
439
  }
440
+ // Provide helpful error if user exists but has no password (social-only account)
337
441
  if (user && !user.passwordHash && user.socialProviders && user.socialProviders.length > 0) {
338
442
  const provider = user.socialProviders[0];
339
443
  const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
@@ -343,6 +447,9 @@ class AuthService {
343
447
  }
344
448
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INVALID_CREDENTIALS, 'Invalid credentials');
345
449
  }
450
+ // ============================================================================
451
+ // Password Expiry Check
452
+ // ============================================================================
346
453
  const expiryDays = this.config.password?.expiryDays;
347
454
  if (expiryDays && expiryDays > 0 && user.passwordChangedAt) {
348
455
  const expiryDate = new Date(user.passwordChangedAt);
@@ -350,10 +457,13 @@ class AuthService {
350
457
  const now = new Date();
351
458
  if (now > expiryDate) {
352
459
  this.logger?.warn?.(`Password expired for user: ${user.sub}. Changed: ${user.passwordChangedAt}, Expiry: ${expiryDate}`);
460
+ // Force password change by setting mustChangePassword flag
353
461
  await this.userRepository.update(user.id, {
354
462
  mustChangePassword: true,
355
463
  });
464
+ // Update in-memory user reference to include mustChangePassword
356
465
  user.mustChangePassword = true;
466
+ // Check challenges - FORCE_CHANGE_PASSWORD will be included
357
467
  const response = await this.challengeHelper.determineAuthResponse({
358
468
  user,
359
469
  config: this.config,
@@ -366,6 +476,11 @@ class AuthService {
366
476
  }
367
477
  }
368
478
  }
479
+ // ============================================================================
480
+ // Audit: Record login attempt for successful password verification
481
+ // ============================================================================
482
+ // Record LOGIN_ATTEMPT for all successful password verifications
483
+ // IMPORTANT: Always await this to ensure correct chronological order before risk assessment
369
484
  try {
370
485
  await this.auditService?.recordEvent({
371
486
  userId: user.id,
@@ -376,18 +491,25 @@ class AuthService {
376
491
  });
377
492
  }
378
493
  catch (auditError) {
494
+ // Non-blocking: Log but continue even if audit fails
379
495
  const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
380
496
  this.logger?.error?.(`Failed to record LOGIN_ATTEMPT audit event: ${errorMessage}`, {
381
497
  error: auditError,
382
498
  userId: user.id,
383
499
  });
384
500
  }
501
+ // ============================================================================
502
+ // Challenge System: Determine authentication response using state machine
503
+ // ============================================================================
504
+ // All challenge determination is now handled by state machine in determineAuthResponse
505
+ // This replaces the old determinePendingChallenges and checkMFARequirement methods
385
506
  const response = await this.challengeHelper.determineAuthResponse({
386
507
  user,
387
508
  config: this.config,
388
509
  deviceToken: clientInfo.deviceToken,
389
510
  isSocialLogin: false,
390
511
  });
512
+ // If challenge is required, record login attempt and return challenge
391
513
  if (response.challengeName) {
392
514
  const reasonMap = {
393
515
  [auth_challenge_dto_1.AuthChallenge.VERIFY_EMAIL]: 'verification_required',
@@ -400,15 +522,20 @@ class AuthService {
400
522
  await this.recordLoginAttempt(dto.identifier, false, reasonMap[response.challengeName] || 'challenge_required', user.id);
401
523
  return response;
402
524
  }
525
+ // If response already has tokens (session was created by challenge helper), return it
526
+ // This prevents duplicate session creation
403
527
  if (response.accessToken && response.refreshToken) {
404
528
  this.logger?.debug?.(`Login successful - session already created by challenge helper for ${dto.identifier} (sub: ${user.sub})`);
529
+ // Record successful login attempt
405
530
  await this.recordLoginAttempt(dto.identifier, true, undefined, user.id);
406
531
  this.logger?.log?.(`Login successful for: ${dto.identifier} (sub: ${user.sub}) from ${clientInfo.ipAddress}`);
532
+ // Update user last login info
407
533
  await this.userRepository.update(user.id, {
408
534
  lastLoginAt: new Date(),
409
535
  lastLoginIp: clientInfo.ipAddress,
410
536
  failedLoginAttempts: 0,
411
537
  });
538
+ // Reset IP-based failed attempts on successful login
412
539
  if (this.config.lockout?.enabled && this.config.lockout.resetOnSuccess) {
413
540
  const ipAddress = clientInfo.ipAddress;
414
541
  if (ipAddress) {
@@ -416,6 +543,7 @@ class AuthService {
416
543
  await this.accountLockoutStorage.resetFailedAttempts(ipAddress);
417
544
  }
418
545
  }
546
+ // Extract session ID and device info from token to record audit event
419
547
  let sessionId;
420
548
  let deviceId;
421
549
  try {
@@ -423,6 +551,7 @@ class AuthService {
423
551
  if (tokenPayload?.sessionId) {
424
552
  sessionId = parseInt(String(tokenPayload.sessionId), 10);
425
553
  }
554
+ // Get deviceId from session if available
426
555
  if (sessionId) {
427
556
  const session = await this.sessionService.findById(sessionId);
428
557
  if (session && session.deviceId) {
@@ -431,11 +560,14 @@ class AuthService {
431
560
  }
432
561
  }
433
562
  catch {
563
+ // Non-blocking: Continue without sessionId/deviceId
434
564
  this.logger?.debug?.('Failed to extract sessionId/deviceId from token for audit');
435
565
  }
566
+ // Determine trusted device and MFA bypass status from response
436
567
  const isTrustedDevice = response.trusted || false;
437
- const mfaBypassed = false;
568
+ const mfaBypassed = false; // Challenge helper handles MFA, so if we get here, MFA was not bypassed
438
569
  const mfaBypassReason = null;
570
+ // Record successful login audit event
439
571
  if (fireAndForget) {
440
572
  this.auditService
441
573
  ?.recordEvent({
@@ -478,6 +610,9 @@ class AuthService {
478
610
  }
479
611
  return response;
480
612
  }
613
+ // ============================================================================
614
+ // Trusted Device Status Check (for audit metadata)
615
+ // ============================================================================
481
616
  let isTrustedDevice = false;
482
617
  let mfaBypassed = false;
483
618
  let mfaBypassReason = null;
@@ -487,20 +622,33 @@ class AuthService {
487
622
  clientInfo.deviceToken) {
488
623
  isTrustedDevice = await this.trustedDeviceService.isDeviceTrusted(clientInfo.deviceToken, user.id);
489
624
  }
625
+ // Check if user is exempt from MFA
490
626
  const userEntityDebug = user;
491
627
  const userMfaExempt = userEntityDebug.mfaExempt === true || userEntityDebug.mfaExempt === 'true';
628
+ // Determine if MFA was bypassed
629
+ // MFA is bypassed if:
630
+ // 1. No challenge was returned (meaning MFA was skipped)
631
+ // 2. MFA would have been required otherwise
632
+ // 3. Either:
633
+ // a. Device is trusted AND bypassMFAForTrustedDevices is enabled (trusted device bypass)
634
+ // b. User has mfaExempt = true (MFA exemption bypass)
492
635
  if (!response.challengeName && this.config.mfa) {
493
636
  const enforcement = this.config.mfa.enforcement || 'OPTIONAL';
637
+ // MFA would be required if:
638
+ // - OPTIONAL enforcement AND user has MFA enabled, OR
639
+ // - REQUIRED/ADAPTIVE enforcement (regardless of user.mfaEnabled for REQUIRED)
494
640
  const wouldRequireMFA = (enforcement === 'OPTIONAL' && user.mfaEnabled) || enforcement === 'REQUIRED' || enforcement === 'ADAPTIVE';
495
641
  if (wouldRequireMFA) {
642
+ // Check if bypassed due to trusted device
496
643
  if (isTrustedDevice &&
497
644
  this.config.mfa.bypassMFAForTrustedDevices === true &&
498
- enforcement !== 'ADAPTIVE' &&
645
+ enforcement !== 'ADAPTIVE' && // Adaptive MFA could bypass it anyway if device is trusted but requires different logging
499
646
  !userMfaExempt) {
500
647
  mfaBypassed = true;
501
648
  mfaBypassReason = 'trusted_device';
502
649
  this.logger?.debug?.(`MFA bypassed for trusted device - user ${user.sub}`);
503
650
  }
651
+ // Check if bypassed due to MFA exemption
504
652
  else if (userMfaExempt) {
505
653
  mfaBypassed = true;
506
654
  mfaBypassReason = 'mfa_exempt';
@@ -508,9 +656,15 @@ class AuthService {
508
656
  }
509
657
  }
510
658
  }
659
+ // MFA challenge is already handled by determineAuthResponse above
660
+ // If response.challengeName is set, it was already returned
661
+ // Check if user is active (should never happen with new signups, but keep for legacy accounts)
511
662
  if (!user.isActive) {
512
663
  this.logger?.warn?.(`Login failed - account inactive: ${dto.identifier} (sub: ${user.sub})`);
513
664
  await this.recordLoginAttempt(dto.identifier, false, 'account_inactive', user.id);
665
+ // ============================================================================
666
+ // Audit: Record blocked login (account inactive)
667
+ // ============================================================================
514
668
  try {
515
669
  await this.auditService?.recordEvent({
516
670
  userId: user.id,
@@ -519,9 +673,11 @@ class AuthService {
519
673
  authMethod: 'password',
520
674
  reason: 'account_inactive',
521
675
  description: 'Login blocked - account is inactive',
676
+ // Client info automatically included from context
522
677
  });
523
678
  }
524
679
  catch (auditError) {
680
+ // Non-blocking: Log but continue
525
681
  const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
526
682
  this.logger?.error?.(`Failed to record LOGIN_BLOCKED audit event (account inactive): ${errorMessage}`, {
527
683
  error: auditError,
@@ -530,6 +686,7 @@ class AuthService {
530
686
  }
531
687
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.ACCOUNT_INACTIVE, 'Account is inactive. Please contact support.');
532
688
  }
689
+ // Reset IP-based failed attempts on successful login
533
690
  if (this.config.lockout?.enabled && this.config.lockout.resetOnSuccess) {
534
691
  const ipAddress = clientInfo.ipAddress;
535
692
  if (ipAddress) {
@@ -537,9 +694,19 @@ class AuthService {
537
694
  await this.accountLockoutStorage.resetFailedAttempts(ipAddress);
538
695
  }
539
696
  }
697
+ // ============================================================================
698
+ // Generate Device ID Server-Side (Security: Never accept from client)
699
+ // ============================================================================
700
+ // Always generate device ID server-side (no client input accepted)
701
+ // This device ID is used for session tracking, not for trusted device feature
702
+ // Trusted devices use separate deviceToken (generated after MFA verification)
540
703
  const validatedDeviceId = crypto.randomUUID();
541
704
  this.logger?.debug?.(`Generated server-side deviceId: ${validatedDeviceId}`);
705
+ // Generate token family for rotation tracking
542
706
  const tokenFamily = this.jwtService.generateTokenFamily();
707
+ // ============================================================================
708
+ // Single Session Mode: Revoke other sessions if disallowMultipleSessions is enabled
709
+ // ============================================================================
543
710
  if (this.config.session?.disallowMultipleSessions) {
544
711
  this.logger?.debug?.(`Single session mode enabled - revoking other sessions for user: ${user.sub}`);
545
712
  const revokedCount = await this.sessionService.revokeAllUserSessions(user.id, 'Login from new session');
@@ -547,6 +714,7 @@ class AuthService {
547
714
  this.logger?.log?.(`Revoked ${revokedCount} other active session(s) for user: ${user.sub}`);
548
715
  }
549
716
  }
717
+ // Atomically create session and persist token hashes
550
718
  this.logger?.debug?.(`Creating login session for user: ${user.sub}`);
551
719
  const atomic = await this.sessionService.createSessionAtomic({
552
720
  userId: user.id,
@@ -554,6 +722,7 @@ class AuthService {
554
722
  deviceId: validatedDeviceId,
555
723
  deviceName: dto.deviceName,
556
724
  deviceType: dto.deviceType,
725
+ // Client info (ipAddress, ipCountry, ipCity, userAgent) automatically extracted from ClientInfoService
557
726
  isRemembered: false,
558
727
  expiresAt: this.sessionService.getSessionExpirationDate(),
559
728
  authMethod: 'password',
@@ -573,13 +742,18 @@ class AuthService {
573
742
  const session = atomic.session;
574
743
  const tokens = atomic.extra;
575
744
  this.logger?.debug?.(`Session created: ${session.id}`);
745
+ // Update user last login info - use internal id for update
576
746
  await this.userRepository.update(user.id, {
577
747
  lastLoginAt: new Date(),
578
748
  lastLoginIp: clientInfo.ipAddress,
579
749
  failedLoginAttempts: 0,
580
750
  });
751
+ // Record successful login attempt - use internal id
581
752
  await this.recordLoginAttempt(dto.identifier, true, undefined, user.id);
582
753
  this.logger?.log?.(`Login successful for: ${dto.identifier} (sub: ${user.sub}) from ${clientInfo.ipAddress}`);
754
+ // ============================================================================
755
+ // Audit: Record successful login with trusted device and MFA bypass metadata
756
+ // ============================================================================
583
757
  if (fireAndForget) {
584
758
  this.auditService
585
759
  ?.recordEvent({
@@ -620,17 +794,26 @@ class AuthService {
620
794
  });
621
795
  }
622
796
  }
797
+ // // Execute afterLogin hook
798
+ // if (this.config.hooks?.afterLogin) {
799
+ // await this.config.hooks.afterLogin(user, session);
800
+ // }
801
+ // ============================================================================
802
+ // Trusted Device Token Management (Remember Device Feature)
803
+ // ============================================================================
623
804
  let deviceToken;
624
805
  let isTrusted = false;
625
806
  if (this.config.mfa?.rememberDevices && this.config.mfa?.rememberDevices !== 'never' && this.trustedDeviceService) {
626
807
  const rememberDevicesMode = this.config.mfa.rememberDevices;
808
+ // Check if device is already trusted
627
809
  if (clientInfo.deviceToken) {
628
810
  isTrusted = await this.trustedDeviceService.isDeviceTrusted(clientInfo.deviceToken, user.id);
629
811
  if (isTrusted) {
630
- deviceToken = clientInfo.deviceToken;
812
+ deviceToken = clientInfo.deviceToken; // Reuse existing token
631
813
  this.logger?.debug?.(`Device already trusted for user ${user.sub}`);
632
814
  }
633
815
  }
816
+ // Auto-trust mode: Create device token automatically if not already trusted
634
817
  if (rememberDevicesMode === 'always' && !isTrusted) {
635
818
  try {
636
819
  deviceToken = await this.trustedDeviceService.createTrustedDevice(user.id, dto.deviceName || clientInfo.deviceName, dto.deviceType || clientInfo.deviceType, clientInfo.ipAddress, clientInfo.userAgent, clientInfo.platform, clientInfo.browser);
@@ -638,13 +821,21 @@ class AuthService {
638
821
  this.logger?.debug?.(`Auto-created trusted device token for user ${user.sub} (always mode)`);
639
822
  }
640
823
  catch (error) {
824
+ // Non-blocking: Log but continue without device token
641
825
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
642
826
  this.logger?.warn?.(`Failed to create trusted device token: ${errorMessage}`, { error });
643
827
  }
644
828
  }
829
+ // user_opt_in mode: Don't create token here - user must call trust-device endpoint
830
+ // isTrusted flag is already set above if device token exists and is valid
645
831
  }
832
+ // Decode tokens to get expiry times
646
833
  const accessTokenValidation = await this.jwtService.validateAccessToken(tokens.accessToken);
647
834
  const refreshTokenValidation = await this.jwtService.validateRefreshToken(tokens.refreshToken);
835
+ // Return sanitized user object with expiry timestamps
836
+ // Note: deviceToken inclusion in response body is handled by CookieTokenInterceptor
837
+ // which checks route-level @TokenDelivery decorator and global config
838
+ // to decide whether to set as cookie and/or strip from body
648
839
  const userDto = user_response_dto_1.UserResponseDto.fromEntity(user);
649
840
  const authResponse = {
650
841
  user: {
@@ -661,19 +852,46 @@ class AuthService {
661
852
  refreshToken: tokens.refreshToken,
662
853
  accessTokenExpiresAt: accessTokenValidation.payload?.exp || 0,
663
854
  refreshTokenExpiresAt: refreshTokenValidation.payload?.exp || 0,
664
- trusted: isTrusted,
855
+ trusted: isTrusted, // Include trusted flag so frontend knows if device is already trusted
856
+ // Include deviceToken - CookieTokenInterceptor will handle cookie/stripping based on @TokenDelivery decorator
665
857
  deviceToken,
666
858
  };
667
859
  return authResponse;
668
860
  }
861
+ /**
862
+ * Complete an authentication challenge using the provided response data.
863
+ *
864
+ * Handles all challenge types (email verification, phone verification, MFA, password change, MFA setup).
865
+ * Validates the session, challenge type, and parameters, and returns the result (tokens or next challenge).
866
+ *
867
+ * @param responseData - Data for responding to the challenge
868
+ * @returns The authentication response (tokens or next challenge requirement)
869
+ * @throws {NAuthException} If validation fails or the challenge type is unknown
870
+ *
871
+ * @example
872
+ * ```typescript
873
+ * // Example for email verification:
874
+ * const dto = Object.assign(new RespondChallengeDTO(), {
875
+ * session: 'session-token',
876
+ * type: 'VERIFY_EMAIL',
877
+ * code: '123456',
878
+ * });
879
+ * await authService.respondToChallenge(dto);
880
+ * ```
881
+ */
669
882
  async respondToChallenge(dto) {
670
883
  const responseData = dto;
671
884
  const { session, type } = responseData;
672
885
  const requestTrace = `${Date.now()}-${Math.random().toString(36).substring(7)}`;
673
886
  this.logger?.log?.(`[${requestTrace}] Challenge response received: type=${type}, session=${session?.substring(0, 8)}...`);
887
+ // Validate session and get challenge type
674
888
  const challengeSession = await this.challengeService.validateSession(session);
889
+ // Validate response matches expected challenge
675
890
  this.validateChallengeTypeMatch(challengeSession.challengeName, type);
891
+ // Validate parameters for this challenge type
892
+ // TODO: Later check if we can use classvalidator to replicate the logic of DTO validation centrally
676
893
  this.validateChallengeParams(type, responseData);
894
+ // Handle challenge based on type
677
895
  switch (type) {
678
896
  case 'VERIFY_EMAIL':
679
897
  return await this.handleVerifyEmail(challengeSession, responseData.code);
@@ -689,11 +907,20 @@ class AuthService {
689
907
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `Unknown challenge type: ${type}`);
690
908
  }
691
909
  }
910
+ /**
911
+ * Validate that response type matches expected challenge type
912
+ */
692
913
  validateChallengeTypeMatch(expected, provided) {
693
914
  if (expected !== provided) {
694
915
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `Challenge type mismatch: expected ${expected}, got ${provided}`);
695
916
  }
696
917
  }
918
+ /**
919
+ * Validate parameters for challenge type
920
+ *
921
+ * Service-level validation ensures Express/other frameworks get same validation as NestJS.
922
+ * This is critical for non-DTO-based applications.
923
+ */
697
924
  validateChallengeParams(type, data) {
698
925
  switch (type) {
699
926
  case 'VERIFY_EMAIL': {
@@ -758,32 +985,42 @@ class AuthService {
758
985
  }
759
986
  }
760
987
  }
988
+ /**
989
+ * Handle VERIFY_EMAIL challenge
990
+ */
761
991
  async handleVerifyEmail(challengeSession, code) {
762
992
  const user = challengeSession.user;
763
993
  if (!user) {
764
994
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.CHALLENGE_INVALID, 'User not found in challenge session');
765
995
  }
766
996
  this.logger?.log?.(`Verifying email for user: ${user.sub}`);
997
+ // Verify email with code, ensuring it belongs to this specific challenge session
767
998
  const verifyDto = Object.assign(new verify_email_dto_1.VerifyEmailWithCodeDTO(), {
768
999
  email: user.email,
769
1000
  code,
770
- challengeSessionId: challengeSession.id,
1001
+ challengeSessionId: challengeSession.id, // Link verification to this specific session
771
1002
  });
772
1003
  const result = await this.emailVerificationService.verifyEmailWithCode(verifyDto);
773
1004
  const isVerified = result.message === 'Email verified successfully. Please log in to continue.';
774
1005
  if (!isVerified) {
1006
+ // Increment attempts but don't consume session
775
1007
  await this.challengeService.incrementAttempts(challengeSession);
776
1008
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VERIFICATION_CODE_INVALID, 'Invalid verification code');
777
1009
  }
1010
+ // Consume challenge session
778
1011
  await this.challengeService.validateAndConsumeSession(challengeSession.sessionToken, auth_challenge_dto_1.AuthChallenge.VERIFY_EMAIL);
1012
+ // Reload user to get updated emailVerified flag
779
1013
  const updatedUser = await this.userRepository.findOne({ where: { sub: user.sub } });
780
1014
  if (!updatedUser) {
781
1015
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found after email verification');
782
1016
  }
1017
+ // Get client info
783
1018
  const clientInfo = this.clientInfoService.get();
1019
+ // Read auth context from challenge session metadata
784
1020
  const authMethod = challengeSession.metadata?.authMethod || 'password';
785
1021
  const authProvider = challengeSession.metadata?.authProvider;
786
1022
  const isSocialLogin = authMethod === 'social';
1023
+ // Check for next challenges
787
1024
  const response = await this.challengeHelper.determineAuthResponse({
788
1025
  user: updatedUser,
789
1026
  config: this.config,
@@ -800,28 +1037,36 @@ class AuthService {
800
1037
  }
801
1038
  return response;
802
1039
  }
1040
+ /**
1041
+ * Handle VERIFY_PHONE challenge
1042
+ */
803
1043
  async handleVerifyPhone(challengeSession, data) {
804
1044
  const user = challengeSession.user;
805
1045
  if (!user) {
806
1046
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.CHALLENGE_INVALID, 'User not found in challenge session');
807
1047
  }
1048
+ // Check if this is phone collection (first step) or verification (second step)
808
1049
  if ('phone' in data && data.phone) {
1050
+ // Phone collection step
809
1051
  const phone = data.phone;
810
1052
  this.logger?.log?.(`Collecting phone number for user: ${user.sub}`);
1053
+ // Validate phone format (E.164 format: +[country][number])
811
1054
  const phoneRegex = /^\+[1-9]\d{1,14}$/;
812
1055
  if (!phoneRegex.test(phone)) {
813
1056
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INVALID_PHONE_FORMAT, 'Invalid phone number format. Use E.164 format (e.g., +1234567890)');
814
1057
  }
1058
+ // Update user phone number
815
1059
  await this.userRepository.update({ sub: user.sub }, { phone });
816
1060
  this.logger?.log?.(`Phone number added for user ${user.sub}: ${phone}`);
1061
+ // Send verification SMS to the newly added phone
817
1062
  let smsError;
818
1063
  if (this.phoneVerificationService) {
819
1064
  this.logger?.log?.(`Sending verification SMS to newly added phone: ${phone}`);
820
1065
  try {
821
1066
  const smsDto = Object.assign(new verify_phone_dto_1.SendVerificationSMSDTO(), {
822
1067
  sub: user.sub,
823
- skipAlreadyVerifiedCheck: false,
824
- challengeSessionId: challengeSession.id,
1068
+ skipAlreadyVerifiedCheck: false, // Explicitly set to false for phone verification (not MFA)
1069
+ challengeSessionId: challengeSession.id, // Link SMS code to this challenge session
825
1070
  });
826
1071
  await this.phoneVerificationService.sendVerificationSMS(smsDto);
827
1072
  this.logger?.log?.(`Verification SMS sent successfully to: ${phone}`);
@@ -836,9 +1081,14 @@ class AuthService {
836
1081
  this.logger?.warn?.(`Phone verification SMS not sent - PhoneVerificationService not available. ` +
837
1082
  'Phone verification requires an SMS provider to be configured.');
838
1083
  }
1084
+ // DO NOT consume the challenge session yet - user still needs to verify the code
1085
+ // Preserve auth context from original challenge session
839
1086
  const authMethod = challengeSession.metadata?.authMethod || 'password';
840
1087
  const authProvider = challengeSession.metadata?.authProvider;
1088
+ // Return same challenge with updated phone in parameters
1089
+ // Skip auto-send since SMS was already sent above during phone collection
841
1090
  const challengeResponse = await this.challengeHelper.createChallengeResponse({ ...user, phone }, auth_challenge_dto_1.AuthChallenge.VERIFY_PHONE, this.config, authMethod, authProvider, true);
1091
+ // Include SMS error in challenge parameters if SMS failed
842
1092
  if (smsError) {
843
1093
  challengeResponse.challengeParameters = challengeResponse.challengeParameters || {};
844
1094
  challengeResponse.challengeParameters.smsError = smsError;
@@ -846,31 +1096,40 @@ class AuthService {
846
1096
  return challengeResponse;
847
1097
  }
848
1098
  else {
1099
+ // Phone verification step (code provided)
849
1100
  const code = data.code;
850
1101
  this.logger?.log?.(`Verifying phone for user: ${user.sub}`);
1102
+ // Check if phone is set
851
1103
  if (!user.phone) {
852
1104
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, 'Phone number not yet provided. Submit phone number first.');
853
1105
  }
1106
+ // Verify phone with code, ensuring it belongs to this specific challenge session
854
1107
  const verifyDto = Object.assign(new verify_phone_by_sub_dto_1.VerifyPhoneWithCodeBySubDTO(), {
855
1108
  sub: user.sub,
856
1109
  code,
857
- challengeSessionId: challengeSession.id,
1110
+ challengeSessionId: challengeSession.id, // Link verification to this specific session
858
1111
  });
859
1112
  const result = await this.phoneVerificationService.verifyPhoneWithCodeBySub(verifyDto);
860
1113
  const isVerified = result.message === 'Phone verified successfully. Please log in to continue.';
861
1114
  if (!isVerified) {
1115
+ // Increment attempts but don't consume session
862
1116
  await this.challengeService.incrementAttempts(challengeSession);
863
1117
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VERIFICATION_CODE_INVALID, 'Invalid verification code');
864
1118
  }
1119
+ // Consume challenge session
865
1120
  await this.challengeService.validateAndConsumeSession(challengeSession.sessionToken, auth_challenge_dto_1.AuthChallenge.VERIFY_PHONE);
1121
+ // Reload user to get updated phoneVerified flag
866
1122
  const updatedUser = await this.userRepository.findOne({ where: { sub: user.sub } });
867
1123
  if (!updatedUser) {
868
1124
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found after phone verification');
869
1125
  }
1126
+ // Get client info
870
1127
  const clientInfo = this.clientInfoService.get();
1128
+ // Read auth context from challenge session metadata
871
1129
  const authMethod = challengeSession.metadata?.authMethod || 'password';
872
1130
  const authProvider = challengeSession.metadata?.authProvider;
873
1131
  const isSocialLogin = authMethod === 'social';
1132
+ // Check for next challenges
874
1133
  const response = await this.challengeHelper.determineAuthResponse({
875
1134
  user: updatedUser,
876
1135
  config: this.config,
@@ -884,6 +1143,9 @@ class AuthService {
884
1143
  }
885
1144
  else {
886
1145
  this.logger?.log?.(`Phone verified, auth completed for: ${user.email}`);
1146
+ // ============================================================================
1147
+ // Audit: Record successful login after phone verification
1148
+ // ============================================================================
887
1149
  const fireAndForget = this.config.auditLogs?.fireAndForget !== false;
888
1150
  if (fireAndForget) {
889
1151
  this.auditService
@@ -929,6 +1191,9 @@ class AuthService {
929
1191
  return response;
930
1192
  }
931
1193
  }
1194
+ /**
1195
+ * Handle MFA_REQUIRED challenge
1196
+ */
932
1197
  async handleMFAVerification(challengeSession, data) {
933
1198
  const user = challengeSession.user;
934
1199
  if (!user) {
@@ -936,18 +1201,23 @@ class AuthService {
936
1201
  }
937
1202
  const method = data.method;
938
1203
  this.logger?.log?.(`MFA verification attempt: method=${method}, user=${user.sub}`);
1204
+ // Check if MFAService is available
939
1205
  if (!this.mfaService) {
940
1206
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INTERNAL_ERROR, 'MFA service is not available');
941
1207
  }
1208
+ // Get client info
942
1209
  const clientInfo = this.clientInfoService.get();
1210
+ // Verify MFA based on method
943
1211
  let isValid = false;
944
1212
  if (method === 'passkey') {
945
1213
  const passkeyData = data;
946
1214
  const credential = passkeyData.credential;
1215
+ // Get expected challenge from session metadata
947
1216
  const expectedChallenge = challengeSession.metadata?.passkeyChallenge;
948
1217
  if (!expectedChallenge) {
949
1218
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.CHALLENGE_INVALID, 'No passkey challenge found in session');
950
1219
  }
1220
+ // Verify passkey via MFAService
951
1221
  const wrappedCredential = { credential, expectedChallenge };
952
1222
  const verifyResult = await this.mfaService.verifyCode({
953
1223
  sub: user.sub,
@@ -959,6 +1229,7 @@ class AuthService {
959
1229
  else {
960
1230
  const codeData = data;
961
1231
  const code = codeData.code;
1232
+ // Verify code via MFAService (handles totp, sms, and backup)
962
1233
  const verifyResult = await this.mfaService.verifyCode({
963
1234
  sub: user.sub,
964
1235
  methodName: method,
@@ -968,6 +1239,7 @@ class AuthService {
968
1239
  }
969
1240
  if (!isValid) {
970
1241
  this.logger?.warn?.(`MFA verification failed for user: ${user.sub}`);
1242
+ // Audit: Record MFA verification failure
971
1243
  if (this.config.auditLogs?.fireAndForget) {
972
1244
  this.auditService
973
1245
  ?.recordEvent({
@@ -1006,10 +1278,12 @@ class AuthService {
1006
1278
  });
1007
1279
  }
1008
1280
  }
1281
+ // Increment challenge attempts (session not consumed, so user can retry)
1009
1282
  await this.challengeService.incrementAttempts(challengeSession);
1010
1283
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VERIFICATION_CODE_INVALID, 'Invalid MFA code');
1011
1284
  }
1012
1285
  this.logger?.log?.(`MFA verified successfully for user: ${user.sub}`);
1286
+ // Audit: Record MFA verification success
1013
1287
  if (this.config.auditLogs?.fireAndForget) {
1014
1288
  this.auditService
1015
1289
  ?.recordEvent({
@@ -1048,17 +1322,28 @@ class AuthService {
1048
1322
  });
1049
1323
  }
1050
1324
  }
1325
+ // Store MFA method in challenge session metadata for CHALLENGE_COMPLETED audit event
1051
1326
  await this.challengeService.updateMetadata(challengeSession.sessionToken, {
1052
1327
  mfaMethod: method,
1053
1328
  });
1329
+ // Only consume the session AFTER successful verification
1054
1330
  await this.challengeService.validateAndConsumeSession(challengeSession.sessionToken, auth_challenge_dto_1.AuthChallenge.MFA_REQUIRED);
1331
+ // Read auth context from challenge session metadata
1055
1332
  const authMethod = challengeSession.metadata?.authMethod || 'password';
1056
1333
  const authProvider = challengeSession.metadata?.authProvider;
1057
1334
  const isSocialLogin = authMethod === 'social';
1335
+ // ============================================================================
1336
+ // Trusted Device Token Management (Remember Device Feature)
1337
+ // ============================================================================
1338
+ // NOTE:
1339
+ // - We only create / update trusted device tokens AFTER MFA has been successfully
1340
+ // verified to avoid trusting devices that haven't completed full auth.
1341
+ // - For 'always' mode, this mirrors the behavior in the primary login flow.
1058
1342
  let deviceToken = clientInfo.deviceToken;
1059
1343
  let isTrustedDevice = false;
1060
1344
  if (this.trustedDeviceService && this.config.mfa?.rememberDevices && this.config.mfa.rememberDevices !== 'never') {
1061
1345
  const rememberMode = this.config.mfa.rememberDevices;
1346
+ // If a device token is already present, check if it's trusted
1062
1347
  if (deviceToken) {
1063
1348
  try {
1064
1349
  isTrustedDevice = await this.trustedDeviceService.isDeviceTrusted(deviceToken, user.id);
@@ -1071,6 +1356,7 @@ class AuthService {
1071
1356
  this.logger?.warn?.(`MFA flow: failed to validate existing trusted device token for user ${user.sub}: ${errorMessage}`, { error });
1072
1357
  }
1073
1358
  }
1359
+ // Auto-trust mode: create device token automatically if not already trusted
1074
1360
  if (rememberMode === 'always' && !isTrustedDevice) {
1075
1361
  try {
1076
1362
  deviceToken = await this.trustedDeviceService.createTrustedDevice(user.id, clientInfo.deviceName, clientInfo.deviceType, clientInfo.ipAddress, clientInfo.userAgent, clientInfo.platform, clientInfo.browser);
@@ -1085,14 +1371,18 @@ class AuthService {
1085
1371
  }
1086
1372
  }
1087
1373
  }
1374
+ // Check for next challenges (MFA is usually the last challenge)
1088
1375
  const response = await this.challengeHelper.determineAuthResponse({
1089
1376
  user,
1090
1377
  config: this.config,
1091
1378
  deviceToken,
1092
1379
  isSocialLogin,
1093
- skipMFAVerification: true,
1380
+ skipMFAVerification: true, // Already verified
1094
1381
  authProvider,
1095
1382
  });
1383
+ // Propagate trusted device metadata into response so that:
1384
+ // - CookieTokenInterceptor can set the nauth_device_token cookie (cookies mode)
1385
+ // - Mobile clients in JSON mode can store the device token securely
1096
1386
  if (isTrustedDevice) {
1097
1387
  response.trusted = response.trusted ?? true;
1098
1388
  }
@@ -1104,6 +1394,9 @@ class AuthService {
1104
1394
  }
1105
1395
  else {
1106
1396
  this.logger?.log?.(`MFA verified, auth completed for: ${user.email}`);
1397
+ // ============================================================================
1398
+ // Audit: Record successful login after MFA completion
1399
+ // ============================================================================
1107
1400
  const fireAndForget = this.config.auditLogs?.fireAndForget !== false;
1108
1401
  if (fireAndForget) {
1109
1402
  this.auditService
@@ -1148,6 +1441,9 @@ class AuthService {
1148
1441
  }
1149
1442
  return response;
1150
1443
  }
1444
+ /**
1445
+ * Handle FORCE_CHANGE_PASSWORD challenge
1446
+ */
1151
1447
  async handleForceChangePassword(challengeSession, newPassword) {
1152
1448
  const user = challengeSession.user;
1153
1449
  if (!user) {
@@ -1167,15 +1463,20 @@ class AuthService {
1167
1463
  description: 'Password changed due to FORCE_CHANGE_PASSWORD challenge',
1168
1464
  },
1169
1465
  });
1466
+ // Consume challenge session
1170
1467
  await this.challengeService.validateAndConsumeSession(challengeSession.sessionToken, auth_challenge_dto_1.AuthChallenge.FORCE_CHANGE_PASSWORD);
1468
+ // Reload user from database to get updated mustChangePassword flag
1171
1469
  const updatedUser = await this.userRepository.findOne({ where: { sub: user.sub } });
1172
1470
  if (!updatedUser) {
1173
1471
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found after password update');
1174
1472
  }
1473
+ // Get client info
1175
1474
  const clientInfo = this.clientInfoService.get();
1475
+ // Read auth context from challenge session metadata
1176
1476
  const authMethod = challengeSession.metadata?.authMethod || 'password';
1177
1477
  const authProvider = challengeSession.metadata?.authProvider;
1178
1478
  const isSocialLogin = authMethod === 'social';
1479
+ // Check for next challenges
1179
1480
  const response = await this.challengeHelper.determineAuthResponse({
1180
1481
  user: updatedUser,
1181
1482
  config: this.config,
@@ -1189,6 +1490,9 @@ class AuthService {
1189
1490
  }
1190
1491
  else {
1191
1492
  this.logger?.log?.(`Password changed, auth completed for: ${user.email}`);
1493
+ // ============================================================================
1494
+ // Audit: Record successful login after password change
1495
+ // ============================================================================
1192
1496
  const fireAndForget = this.config.auditLogs?.fireAndForget !== false;
1193
1497
  if (fireAndForget) {
1194
1498
  this.auditService
@@ -1233,6 +1537,9 @@ class AuthService {
1233
1537
  }
1234
1538
  return response;
1235
1539
  }
1540
+ /**
1541
+ * Handle MFA_SETUP_REQUIRED challenge
1542
+ */
1236
1543
  async handleMFASetup(challengeSession, data) {
1237
1544
  const user = challengeSession.user;
1238
1545
  if (!user) {
@@ -1242,10 +1549,13 @@ class AuthService {
1242
1549
  const setupData = data.setupData;
1243
1550
  const requestTrace = `${Date.now()}-${Math.random().toString(36).substring(7)}`;
1244
1551
  this.logger?.log?.(`[${requestTrace}] MFA setup attempt: method=${method}, user=${user.sub}`);
1552
+ // Check if MFAService is available
1245
1553
  if (!this.mfaService) {
1246
1554
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INTERNAL_ERROR, 'MFA service is not available');
1247
1555
  }
1556
+ // Get provider
1248
1557
  const provider = this.mfaService.getProvider(method);
1558
+ // Verify setup based on method
1249
1559
  let deviceId;
1250
1560
  try {
1251
1561
  deviceId = await provider.verifySetup(user, setupData);
@@ -1253,30 +1563,41 @@ class AuthService {
1253
1563
  }
1254
1564
  catch (error) {
1255
1565
  this.logger?.warn?.(`MFA setup verification failed: method=${method}, user=${user.sub}`);
1566
+ // Increment attempts but don't consume session
1256
1567
  await this.challengeService.incrementAttempts(challengeSession);
1568
+ // Re-throw the error
1257
1569
  throw error;
1258
1570
  }
1571
+ // Store MFA method in challenge session metadata for CHALLENGE_COMPLETED audit event
1259
1572
  await this.challengeService.updateMetadata(challengeSession.sessionToken, {
1260
1573
  mfaMethod: method,
1261
1574
  });
1575
+ // Consume challenge session
1262
1576
  await this.challengeService.validateAndConsumeSession(challengeSession.sessionToken, auth_challenge_dto_1.AuthChallenge.MFA_SETUP_REQUIRED);
1577
+ // Reload user from database to get updated mfaEnabled flag
1263
1578
  const updatedUser = await this.userRepository.findOne({ where: { sub: user.sub } });
1264
1579
  if (!updatedUser) {
1265
1580
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found after MFA setup');
1266
1581
  }
1582
+ // Get client info
1267
1583
  const clientInfo = this.clientInfoService.get();
1584
+ // Check for next challenges with updated user data
1585
+ // Skip MFA verification because device was already verified during setup
1268
1586
  const response = await this.challengeHelper.determineAuthResponse({
1269
1587
  user: updatedUser,
1270
1588
  config: this.config,
1271
1589
  deviceToken: clientInfo.deviceToken,
1272
1590
  isSocialLogin: false,
1273
- skipMFAVerification: true,
1591
+ skipMFAVerification: true, // Device already verified during setup
1274
1592
  });
1275
1593
  if (response.challengeName) {
1276
1594
  this.logger?.log?.(`Additional challenge required: ${response.challengeName}`);
1277
1595
  }
1278
1596
  else {
1279
1597
  this.logger?.log?.(`MFA setup completed, auth completed for: ${user.email}`);
1598
+ // ============================================================================
1599
+ // Audit: Record successful login after MFA setup
1600
+ // ============================================================================
1280
1601
  const fireAndForget = this.config.auditLogs?.fireAndForget !== false;
1281
1602
  if (fireAndForget) {
1282
1603
  this.auditService
@@ -1321,15 +1642,42 @@ class AuthService {
1321
1642
  }
1322
1643
  return response;
1323
1644
  }
1645
+ // ============================================================================
1646
+ // Challenge Helper Methods
1647
+ // ============================================================================
1648
+ /**
1649
+ * Resend verification code for current challenge
1650
+ *
1651
+ * Determines the challenge type from the session and resends the appropriate code:
1652
+ * - VERIFY_EMAIL: Resends email verification code
1653
+ * - VERIFY_PHONE: Resends SMS verification code
1654
+ * - MFA_REQUIRED: Resends MFA code (for SMS MFA)
1655
+ *
1656
+ * Rate limits are enforced internally by the verification services.
1657
+ *
1658
+ * @param session - Challenge session token
1659
+ * @returns Destination info (masked email/phone)
1660
+ * @throws {NAuthException} INVALID_CHALLENGE_SESSION | RATE_LIMIT_* | VALIDATION_FAILED
1661
+ *
1662
+ * @example
1663
+ * ```typescript
1664
+ * const result = await authService.resendCode(session);
1665
+ * // Returns: { destination: 'u***r@example.com' }
1666
+ * ```
1667
+ */
1324
1668
  async resendCode(dto) {
1325
1669
  this.logger?.debug?.(`Resending verification code: session=${dto.session}`);
1670
+ // Validate session (session must be valid to resend)
1326
1671
  const challengeSession = await this.challengeService.validateSession(dto.session);
1672
+ // Get user from session
1327
1673
  const user = challengeSession.user;
1328
1674
  if (!user) {
1329
1675
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, 'Challenge session has no associated user');
1330
1676
  }
1677
+ // Handle based on challenge type
1331
1678
  switch (challengeSession.challengeName) {
1332
1679
  case auth_challenge_dto_1.AuthChallenge.VERIFY_EMAIL: {
1680
+ // Resend email verification
1333
1681
  const resendDto = Object.assign(new verify_email_dto_1.ResendVerificationEmailDTO(), { sub: user.sub });
1334
1682
  await this.emailVerificationService.resendVerificationEmail(resendDto);
1335
1683
  const maskedEmail = this.maskEmail(user.email);
@@ -1337,12 +1685,14 @@ class AuthService {
1337
1685
  return { destination: maskedEmail };
1338
1686
  }
1339
1687
  case auth_challenge_dto_1.AuthChallenge.VERIFY_PHONE: {
1688
+ // Check if phone already collected
1340
1689
  if (!user.phone) {
1341
1690
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, 'Phone number not yet provided. Submit phone number first.');
1342
1691
  }
1343
1692
  if (!this.phoneVerificationService) {
1344
1693
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INTERNAL_ERROR, 'Phone verification service is not available');
1345
1694
  }
1695
+ // Resend SMS verification
1346
1696
  const resendDto = Object.assign(new verify_phone_dto_1.ResendVerificationSMSDTO(), { sub: user.sub });
1347
1697
  await this.phoneVerificationService.resendVerificationSMS(resendDto);
1348
1698
  const maskedPhone = this.maskPhone(user.phone);
@@ -1350,33 +1700,41 @@ class AuthService {
1350
1700
  return { destination: maskedPhone };
1351
1701
  }
1352
1702
  case auth_challenge_dto_1.AuthChallenge.MFA_REQUIRED: {
1703
+ // For MFA, we need to know which method is being used
1704
+ // Method is stored in metadata when challenge is created (see auth-challenge-helper.service.ts line 403)
1705
+ // Note: challengeParameters is never populated - only metadata is used
1353
1706
  const metadata = challengeSession.metadata;
1354
1707
  const method = metadata?.method;
1355
1708
  if (!method) {
1356
1709
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, 'Cannot resend MFA code: method not specified in session');
1357
1710
  }
1711
+ // SMS and Email MFA support resending codes
1358
1712
  if (method === 'sms' || method === 'email') {
1713
+ // For SMS, use phone verification service directly to pass challengeSessionId
1359
1714
  if (method === 'sms' && this.phoneVerificationService) {
1360
1715
  const smsDto = Object.assign(new verify_phone_dto_1.SendVerificationSMSDTO(), {
1361
1716
  sub: user.sub,
1362
1717
  skipAlreadyVerifiedCheck: true,
1363
- challengeSessionId: challengeSession.id,
1718
+ challengeSessionId: challengeSession.id, // Link resend code to this challenge session
1364
1719
  });
1365
1720
  await this.phoneVerificationService.sendVerificationSMS(smsDto);
1366
1721
  this.logger?.debug?.(`SMS MFA code resent: user=${user.sub}`);
1722
+ // Get masked phone from user or device
1367
1723
  const maskedPhone = user.phone ? this.maskPhone(user.phone) : '***-***-****';
1368
1724
  return { destination: maskedPhone };
1369
1725
  }
1726
+ // For Email, use email verification service directly to pass challengeSessionId
1370
1727
  if (method === 'email' && this.emailVerificationService) {
1371
1728
  const emailDto = Object.assign(new verify_email_dto_1.ResendVerificationEmailDTO(), {
1372
1729
  sub: user.sub,
1373
- challengeSessionId: challengeSession.id,
1730
+ challengeSessionId: challengeSession.id, // Link resend code to this challenge session
1374
1731
  });
1375
1732
  await this.emailVerificationService.resendVerificationEmail(emailDto);
1376
1733
  this.logger?.debug?.(`Email MFA code resent: user=${user.sub}`);
1377
1734
  const maskedEmail = user.email ? this.maskEmail(user.email) : 'u***r@example.com';
1378
1735
  return { destination: maskedEmail };
1379
1736
  }
1737
+ // Fallback to provider if services not available (shouldn't happen)
1380
1738
  if (!this.mfaService) {
1381
1739
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INTERNAL_ERROR, 'MFA service is not available');
1382
1740
  }
@@ -1386,6 +1744,7 @@ class AuthService {
1386
1744
  }
1387
1745
  const result = await provider.sendChallenge(user);
1388
1746
  this.logger?.debug?.(`${method.toUpperCase()} MFA code resent: user=${user.sub}`);
1747
+ // Provider returns masked phone or email
1389
1748
  return { destination: result };
1390
1749
  }
1391
1750
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `Cannot resend code for MFA method '${method}'. Only SMS and Email support code resending.`);
@@ -1394,6 +1753,9 @@ class AuthService {
1394
1753
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `Cannot resend code for challenge type '${challengeSession.challengeName}'`);
1395
1754
  }
1396
1755
  }
1756
+ /**
1757
+ * Mask email for display (helper method)
1758
+ */
1397
1759
  maskEmail(email) {
1398
1760
  const [localPart, domain] = email.split('@');
1399
1761
  if (localPart.length <= 2) {
@@ -1401,11 +1763,30 @@ class AuthService {
1401
1763
  }
1402
1764
  return `${localPart[0]}***${localPart[localPart.length - 1]}@${domain}`;
1403
1765
  }
1766
+ /**
1767
+ * Mask phone number for display (helper method)
1768
+ */
1404
1769
  maskPhone(phone) {
1405
1770
  const digits = phone.replace(/\D/g, '');
1406
1771
  const lastFour = digits.slice(-4);
1407
1772
  return `***-***-${lastFour}`;
1408
1773
  }
1774
+ /**
1775
+ * Registers the current device as trusted for the user (opt-in).
1776
+ *
1777
+ * Only available when rememberDevices is set to 'user_opt_in'. Generates and returns a trusted device token for the device associated with the current authenticated session.
1778
+ *
1779
+ * Session ID is automatically extracted from the JWT token context (via ClientInfoService), similar to how IP address and user agent are handled.
1780
+ *
1781
+ * @returns Object containing the new device token
1782
+ * @throws {NAuthException} If the feature is unavailable, service is not enabled, or session ID is not available
1783
+ *
1784
+ * @example
1785
+ * ```typescript
1786
+ * const result = await authService.trustDevice();
1787
+ * // { deviceToken: 'abc123' }
1788
+ * ```
1789
+ */
1409
1790
  async trustDevice() {
1410
1791
  if (this.config.mfa?.rememberDevices !== 'user_opt_in') {
1411
1792
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.FORBIDDEN, 'Trust device feature is only available in user_opt_in mode');
@@ -1413,19 +1794,23 @@ class AuthService {
1413
1794
  if (!this.trustedDeviceService) {
1414
1795
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INTERNAL_ERROR, 'Trusted device service not available');
1415
1796
  }
1797
+ // Get sessionId from context (automatically extracted from JWT token)
1416
1798
  const clientInfo = this.clientInfoService.get();
1417
1799
  const sessionId = clientInfo.sessionId;
1418
1800
  if (!sessionId) {
1419
1801
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session ID not found in request context. Ensure the request is authenticated.');
1420
1802
  }
1803
+ // Get session to extract device info
1421
1804
  const session = await this.sessionService.findById(sessionId);
1422
1805
  if (!session || session.isRevoked) {
1423
1806
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session not found or revoked');
1424
1807
  }
1808
+ // Get user
1425
1809
  const user = await this.userRepository.findOne({ where: { id: session.userId } });
1426
1810
  if (!user) {
1427
1811
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
1428
1812
  }
1813
+ // Check if device is already trusted
1429
1814
  const userId = typeof user.id === 'number' ? user.id : parseInt(String(user.id), 10);
1430
1815
  if (clientInfo.deviceToken) {
1431
1816
  const isAlreadyTrusted = await this.trustedDeviceService.isDeviceTrusted(clientInfo.deviceToken, userId);
@@ -1433,24 +1818,33 @@ class AuthService {
1433
1818
  this.logger?.debug?.(`Device already trusted for user ${user.sub}`);
1434
1819
  return { deviceToken: clientInfo.deviceToken };
1435
1820
  }
1821
+ // If device token exists but not trusted, revoke it first (may be expired/invalid)
1436
1822
  try {
1437
1823
  await this.trustedDeviceService.revokeTrustedDevice(clientInfo.deviceToken, userId);
1438
1824
  this.logger?.debug?.(`Revoked existing untrusted device token for user ${user.sub}`);
1439
1825
  }
1440
1826
  catch {
1827
+ // Non-blocking - may not exist
1441
1828
  }
1442
1829
  }
1830
+ // Create trusted device token using session device info
1443
1831
  const deviceToken = await this.trustedDeviceService.createTrustedDevice(userId, session.deviceName || clientInfo.deviceName, session.deviceType || clientInfo.deviceType, session.ipAddress || clientInfo.ipAddress, session.userAgent || clientInfo.userAgent, clientInfo.platform, clientInfo.browser);
1444
1832
  this.logger?.log?.(`Device trusted for user ${user.sub} (user opt-in)`);
1833
+ // ============================================================================
1834
+ // Audit: Record device trust event
1835
+ // ============================================================================
1445
1836
  try {
1837
+ // Ensure userId is a number for audit
1446
1838
  const userId = typeof user.id === 'number' ? user.id : parseInt(String(user.id), 10);
1447
1839
  await this.auditService?.recordEvent({
1448
1840
  userId,
1449
1841
  eventType: auth_audit_event_type_enum_1.AuthAuditEventType.DEVICE_TRUSTED,
1450
1842
  eventStatus: 'SUCCESS',
1843
+ // Override deviceId with the newly created device token
1451
1844
  deviceId: deviceToken,
1452
1845
  sessionId: session.id,
1453
1846
  description: `Device trusted by user (opt-in) - ${session.deviceName || 'Unknown device'}`,
1847
+ // Client info (deviceName, deviceType, etc.) automatically included from context
1454
1848
  metadata: {
1455
1849
  rememberDeviceDays: this.config.mfa?.rememberDeviceDays || 30,
1456
1850
  trustedUntil: new Date(Date.now() + (this.config.mfa?.rememberDeviceDays || 30) * 24 * 60 * 60 * 1000).toISOString(),
@@ -1458,6 +1852,7 @@ class AuthService {
1458
1852
  });
1459
1853
  }
1460
1854
  catch (auditError) {
1855
+ // Non-blocking: Log but continue
1461
1856
  const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
1462
1857
  this.logger?.error?.(`Failed to record DEVICE_TRUSTED audit event: ${errorMessage}`, {
1463
1858
  error: auditError,
@@ -1466,23 +1861,49 @@ class AuthService {
1466
1861
  }
1467
1862
  return { deviceToken };
1468
1863
  }
1864
+ /**
1865
+ * Check if the current device is trusted
1866
+ *
1867
+ * Returns whether the device associated with the current authenticated session
1868
+ * is trusted. Works for both cookies mode (reads from httpOnly cookie) and
1869
+ * JSON mode (reads from X-Device-Token header).
1870
+ *
1871
+ * This endpoint validates the device token on the server side and checks:
1872
+ * - Device token exists and is valid
1873
+ * - Device token matches a trusted device record in the database
1874
+ * - Trust has not expired
1875
+ *
1876
+ * @returns Object containing the trusted status
1877
+ * @throws {NAuthException} If the session is not found or user is not authenticated
1878
+ *
1879
+ * @example
1880
+ * ```typescript
1881
+ * const result = await authService.isTrustedDevice();
1882
+ * // { trusted: true }
1883
+ * ```
1884
+ */
1469
1885
  async isTrustedDevice() {
1470
1886
  if (!this.trustedDeviceService) {
1887
+ // If trusted device service is not available, device is not trusted
1471
1888
  return { trusted: false };
1472
1889
  }
1890
+ // Get sessionId from context (automatically extracted from JWT token)
1473
1891
  const clientInfo = this.clientInfoService.get();
1474
1892
  const sessionId = clientInfo.sessionId;
1475
1893
  if (!sessionId) {
1476
1894
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session ID not found in request context. Ensure the request is authenticated.');
1477
1895
  }
1896
+ // Get session to extract user
1478
1897
  const session = await this.sessionService.findById(sessionId);
1479
1898
  if (!session || session.isRevoked) {
1480
1899
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session not found or revoked');
1481
1900
  }
1901
+ // Get user
1482
1902
  const user = await this.userRepository.findOne({ where: { id: session.userId } });
1483
1903
  if (!user) {
1484
1904
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
1485
1905
  }
1906
+ // Check if device is trusted
1486
1907
  const userId = typeof user.id === 'number' ? user.id : parseInt(String(user.id), 10);
1487
1908
  const deviceToken = clientInfo.deviceToken;
1488
1909
  if (!deviceToken) {
@@ -1491,15 +1912,40 @@ class AuthService {
1491
1912
  const isTrusted = await this.trustedDeviceService.isDeviceTrusted(deviceToken, userId);
1492
1913
  return { trusted: isTrusted };
1493
1914
  }
1915
+ /**
1916
+ * Refresh the access token using a refresh token.
1917
+ *
1918
+ * Handles secure token rotation with distributed locking, reuse detection,
1919
+ * and family revocation to prevent race conditions and replay attacks.
1920
+ *
1921
+ * @param refreshToken - The refresh token issued to the client
1922
+ * @returns Newly generated access and refresh tokens
1923
+ * @throws {NAuthException} If the session is not found, revoked, or refresh is abused
1924
+ *
1925
+ * @example
1926
+ * ```typescript
1927
+ * const tokens = await authService.refreshToken(refreshToken);
1928
+ * ```
1929
+ */
1494
1930
  async refreshToken(dto) {
1495
1931
  const tokenHash = this.jwtService.hashToken(dto.refreshToken);
1932
+ // ============================================================================
1933
+ // CRITICAL SECURITY FIX #1 & #2: Distributed Lock + Reuse Detection
1934
+ // ============================================================================
1935
+ // CRITICAL: We need to get session ID for locking, but we must lock BEFORE validation
1936
+ // to prevent race conditions. So we do a quick, lightweight lookup first.
1937
+ // Find session by refresh token hash - this is fast and allows us to get session ID
1496
1938
  const session = await this.sessionService.findByRefreshToken(tokenHash);
1497
1939
  if (!session || session.isRevoked) {
1940
+ // Validate token to get user info for error message
1498
1941
  const validation = await this.jwtService.validateRefreshToken(dto.refreshToken);
1499
1942
  const userId = validation.payload?.sub || 'unknown';
1500
1943
  this.logger?.debug?.(`Session not found or revoked for user ${userId}. Possible issue where token are not cleared on logout`);
1501
1944
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session not found or revoked');
1502
1945
  }
1946
+ // Acquire distributed lock using SESSION ID (not token hash)
1947
+ // THIS MUST HAPPEN BEFORE VALIDATION to prevent race conditions
1948
+ // where multiple requests validate the same token before any lock is acquired
1503
1949
  const lockKey = `session-refresh:${session.id}`;
1504
1950
  this.logger?.debug?.(`[REFRESH DEBUG] Attempting to acquire lock ${lockKey} for token hash ${tokenHash.substring(0, 16)}...`);
1505
1951
  let lockAcquired = false;
@@ -1514,32 +1960,50 @@ class AuthService {
1514
1960
  });
1515
1961
  }
1516
1962
  this.logger?.debug?.(`[REFRESH DEBUG] Lock ${lockKey} acquired successfully in ${lockDuration}ms for token hash ${tokenHash.substring(0, 16)}...`);
1963
+ // CRITICAL: Check for token reuse IMMEDIATELY after acquiring lock
1964
+ // If same session + cookie race → return current tokens (don't reissue)
1965
+ // If different session → invalidate that session and reject (attack)
1517
1966
  if (this.config.jwt.refreshToken.reuseDetection) {
1518
1967
  const isAlreadyUsed = await this.sessionService.isRefreshTokenUsed(tokenHash);
1519
1968
  if (isAlreadyUsed) {
1969
+ // Decode token to get sessionId from JWT payload (without full validation)
1970
+ // This allows us to check if the token belongs to the session we found
1520
1971
  const tokenPayload = this.jwtService.decodeToken(dto.refreshToken);
1521
1972
  const tokenSessionId = tokenPayload?.sessionId;
1973
+ // Get current session state to ensure it's still valid
1522
1974
  const currentSession = (await this.sessionService.findByIdLight(session.id));
1523
1975
  if (!currentSession || currentSession.isRevoked) {
1524
1976
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session not found or revoked');
1525
1977
  }
1978
+ // Check if token's sessionId matches the session we found
1979
+ // If they match → cookie race (same session)
1980
+ // If they don't match → attack (token stolen from different session)
1526
1981
  if (tokenSessionId && tokenSessionId === session.id.toString()) {
1982
+ // Same session - this is a cookie race condition
1983
+ // Return the current valid tokens (user already has them from first request)
1527
1984
  this.logger?.debug?.(`[REFRESH DEBUG] Token hash ${tokenHash.substring(0, 16)}... already used for same session ${session.id} - cookie race detected, returning current tokens`);
1985
+ // Get user info
1528
1986
  const user = (await this.userRepository.findOne({
1529
1987
  where: { id: currentSession.userId },
1530
1988
  }));
1531
1989
  if (!user) {
1532
1990
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
1533
1991
  }
1992
+ // Generate tokens from current session state (same as what the first request returned)
1993
+ // These will match what the user already has, so no change needed
1994
+ // Note: deviceId not included in token - session.deviceId is source of truth
1534
1995
  const newTokens = await this.jwtService.generateTokenPair({
1535
1996
  userId: user.sub,
1536
1997
  email: user.email,
1537
1998
  sessionId: currentSession.id.toString(),
1538
1999
  tokenFamily: currentSession.tokenFamily,
1539
2000
  });
2001
+ // Update session with these tokens (they're already there, but ensures consistency)
1540
2002
  await this.sessionService.updateTokens(currentSession.id, this.jwtService.hashToken(newTokens.accessToken), this.jwtService.hashToken(newTokens.refreshToken));
2003
+ // Decode tokens to get expiry times
1541
2004
  const accessTokenValidation = await this.jwtService.validateAccessToken(newTokens.accessToken);
1542
2005
  const refreshTokenValidation = await this.jwtService.validateRefreshToken(newTokens.refreshToken);
2006
+ // Return success with current tokens
1543
2007
  return {
1544
2008
  accessToken: newTokens.accessToken,
1545
2009
  refreshToken: newTokens.refreshToken,
@@ -1548,8 +2012,12 @@ class AuthService {
1548
2012
  };
1549
2013
  }
1550
2014
  else {
2015
+ // Different session - this is an attack!
2016
+ // A refresh token from one session cannot be used by another session
1551
2017
  this.logger?.error?.(`[REFRESH DEBUG] Token hash ${tokenHash.substring(0, 16)}... already used for different session - ATTACK DETECTED! Token sessionId: ${tokenSessionId}, Found session: ${session.id}. Revoking session ${session.id}`);
2018
+ // Revoke the session that's trying to use a stolen token
1552
2019
  await this.sessionService.revokeSession(session.id, 'Token reuse detected - possible token theft');
2020
+ // Audit the attack
1553
2021
  let userForAudit = null;
1554
2022
  try {
1555
2023
  userForAudit = (await this.userRepository.findOne({
@@ -1563,6 +2031,7 @@ class AuthService {
1563
2031
  riskFactor: 90,
1564
2032
  riskFactors: [risk_factor_enum_1.RiskFactor.TOKEN_THEFT_ATTEMPT, risk_factor_enum_1.RiskFactor.REFRESH_TOKEN_REUSE_DIFFERENT_SESSION],
1565
2033
  reason: 'Refresh token reuse from different session',
2034
+ // Client info automatically included from context
1566
2035
  description: 'Refresh token from another session attempted to be used. Session revoked as security measure.',
1567
2036
  metadata: {
1568
2037
  sessionId: session.id,
@@ -1585,21 +2054,34 @@ class AuthService {
1585
2054
  }
1586
2055
  }
1587
2056
  }
2057
+ // NOW validate the refresh token (after lock is acquired and reuse check)
2058
+ // This ensures only one request can validate at a time per session
1588
2059
  const validation = await this.jwtService.validateRefreshToken(dto.refreshToken);
1589
2060
  if (!validation.valid || !validation.payload) {
1590
2061
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.TOKEN_INVALID, 'Invalid refresh token');
1591
2062
  }
1592
2063
  const payload = validation.payload;
2064
+ // Re-check session after acquiring lock (it might have been revoked/updated)
2065
+ // Since we have the lock, no other request can modify this session, but it might have been revoked
2066
+ // We already have currentSession from the early reuse check, but re-fetch to ensure it's still valid
1593
2067
  const lockedSession = (await this.sessionService.findByIdLight(session.id));
1594
2068
  if (!lockedSession || lockedSession.isRevoked || lockedSession.id !== session.id) {
1595
2069
  this.logger?.debug?.(`Session changed after lock acquisition for user ${payload.sub}. Session may have been revoked.`);
1596
2070
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session not found or revoked');
1597
2071
  }
2072
+ // ============================================================================
2073
+ // NOTE: We still do the atomic mark operation below as a double-check
2074
+ // The early check above handles cookie race conditions where old tokens
2075
+ // are sent before new cookies are received
2076
+ // ============================================================================
2077
+ // Mark token as used BEFORE generating new tokens (prevents reuse)
1598
2078
  if (this.config.jwt.refreshToken.reuseDetection) {
1599
2079
  const refreshTokenTTL = this.jwtService.getRefreshTokenTTL();
1600
2080
  const marked = await this.sessionService.markRefreshTokenAsUsed(tokenHash, refreshTokenTTL);
1601
2081
  if (!marked) {
2082
+ // Token was already marked as used - reuse detected!
1602
2083
  this.logger?.error?.(`Token reuse detected for user ${payload.sub} - atomic mark failed, revoking entire token family ${payload.tokenFamily}`);
2084
+ // Audit the reuse attempt
1603
2085
  try {
1604
2086
  const userForAudit = (await this.userRepository.findOne({
1605
2087
  where: { sub: payload.sub },
@@ -1612,6 +2094,7 @@ class AuthService {
1612
2094
  riskFactor: 75,
1613
2095
  riskFactors: [risk_factor_enum_1.RiskFactor.TOKEN_REUSE_ATTEMPT],
1614
2096
  reason: 'Token reuse attempt blocked',
2097
+ // Client info automatically included from context
1615
2098
  description: 'Refresh token reuse attempt detected via atomic operation. Legitimate user session preserved.',
1616
2099
  metadata: {
1617
2100
  tokenFamily: payload.tokenFamily,
@@ -1628,14 +2111,19 @@ class AuthService {
1628
2111
  }
1629
2112
  this.logger?.debug?.(`Marked refresh token as used for session ${lockedSession.id}`);
1630
2113
  }
2114
+ // Generate new token pair with same family
2115
+ // Note: deviceId not included in token - session.deviceId is source of truth
1631
2116
  const newTokens = await this.jwtService.generateTokenPair({
1632
2117
  userId: payload.sub,
1633
2118
  email: payload.email,
1634
- sessionId: lockedSession.id.toString(),
2119
+ sessionId: lockedSession.id.toString(), // Convert integer to string for JWT
1635
2120
  tokenFamily: payload.tokenFamily,
1636
2121
  });
2122
+ // Update session with new token hashes (token rotation)
2123
+ // This automatically invalidates the old tokens as they won't match the session
1637
2124
  await this.sessionService.updateTokens(lockedSession.id, this.jwtService.hashToken(newTokens.accessToken), this.jwtService.hashToken(newTokens.refreshToken));
1638
2125
  this.logger?.log?.(`Token refreshed successfully for user ${payload.sub}`);
2126
+ // Decode new tokens to get expiry times
1639
2127
  const accessTokenValidation = await this.jwtService.validateAccessToken(newTokens.accessToken);
1640
2128
  const refreshTokenValidation = await this.jwtService.validateRefreshToken(newTokens.refreshToken);
1641
2129
  return {
@@ -1646,22 +2134,40 @@ class AuthService {
1646
2134
  };
1647
2135
  }
1648
2136
  finally {
2137
+ // Always release lock, even if error occurs
2138
+ // Only release if we successfully acquired it
1649
2139
  if (lockAcquired) {
1650
2140
  await this.sessionService.releaseRefreshLock(lockKey);
1651
2141
  this.logger?.debug?.(`[REFRESH DEBUG] Released lock ${lockKey}`);
1652
2142
  }
1653
2143
  }
1654
2144
  }
2145
+ // ============================================================================
2146
+ // Logout
2147
+ // ============================================================================
2148
+ /**
2149
+ * Logout user (revoke session)
2150
+ *
2151
+ * Session ID is automatically extracted from the JWT token context (via ClientInfoService), similar to how IP address and user agent are handled.
2152
+ *
2153
+ * @param dto - Logout options (forgetMe flag)
2154
+ * @returns Success status
2155
+ * @throws {NAuthException} If session ID is not available in request context
2156
+ */
1655
2157
  async logout(dto) {
2158
+ // Get sessionId from context (automatically extracted from JWT token)
1656
2159
  const clientInfo = this.clientInfoService.get();
1657
2160
  let sessionId = clientInfo.sessionId;
2161
+ // Fallback: Try to get sessionId from JWT payload in context
1658
2162
  if (!sessionId) {
1659
2163
  const jwtPayload = context_storage_1.ContextStorage.get('JWT_PAYLOAD');
1660
2164
  if (jwtPayload?.sessionId) {
2165
+ // Parse sessionId to number (JWT payload has it as string)
1661
2166
  const sessionIdStr = String(jwtPayload.sessionId);
1662
2167
  const sessionIdNumber = parseInt(sessionIdStr, 10);
1663
2168
  if (!isNaN(sessionIdNumber) && sessionIdNumber > 0) {
1664
2169
  sessionId = sessionIdNumber;
2170
+ // Update CLIENT_INFO in context for future use
1665
2171
  const clientInfoInContext = context_storage_1.ContextStorage.get('CLIENT_INFO');
1666
2172
  if (clientInfoInContext) {
1667
2173
  clientInfoInContext.sessionId = sessionIdNumber;
@@ -1673,6 +2179,7 @@ class AuthService {
1673
2179
  if (!sessionId) {
1674
2180
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session ID not found in request context. Ensure the request is authenticated.');
1675
2181
  }
2182
+ // Prepare metadata for audit trail
1676
2183
  const auditMetadata = dto.forgetMe
1677
2184
  ? {
1678
2185
  deviceForgotten: true,
@@ -1680,19 +2187,26 @@ class AuthService {
1680
2187
  }
1681
2188
  : undefined;
1682
2189
  await this.sessionService.revokeSession(sessionId, 'User logout', auditMetadata);
2190
+ // If forgetMe is true, revoke trusted device
1683
2191
  if (dto.forgetMe &&
1684
2192
  this.config.mfa?.rememberDevices &&
1685
2193
  this.config.mfa?.rememberDevices !== 'never' &&
1686
2194
  this.trustedDeviceService) {
1687
2195
  if (clientInfo.deviceToken) {
1688
2196
  try {
2197
+ // Get session to get userId
1689
2198
  const session = await this.sessionService.findById(sessionId);
1690
2199
  if (session) {
1691
2200
  await this.trustedDeviceService.revokeTrustedDevice(clientInfo.deviceToken, session.userId);
1692
2201
  this.logger?.log?.(`Revoked trusted device token for user (forgetMe=true)`);
2202
+ // Get user for audit
1693
2203
  const user = await this.userRepository.findOne({ where: { id: session.userId } });
1694
2204
  if (user) {
2205
+ // ============================================================================
2206
+ // Audit: Record device untrust event
2207
+ // ============================================================================
1695
2208
  try {
2209
+ // Ensure userId is a number for audit
1696
2210
  const userId = typeof user.id === 'number' ? user.id : parseInt(String(user.id), 10);
1697
2211
  await this.auditService?.recordEvent({
1698
2212
  userId,
@@ -1700,12 +2214,14 @@ class AuthService {
1700
2214
  eventStatus: 'SUCCESS',
1701
2215
  sessionId: session.id,
1702
2216
  description: `Device untrusted by user (forgetMe=true) - ${session.deviceName || 'Unknown device'}`,
2217
+ // Client info (deviceId, deviceName, deviceType, etc.) automatically included from context
1703
2218
  metadata: {
1704
2219
  reason: 'user_logout_forget_me',
1705
2220
  },
1706
2221
  });
1707
2222
  }
1708
2223
  catch (auditError) {
2224
+ // Non-blocking: Log but continue
1709
2225
  const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
1710
2226
  this.logger?.error?.(`Failed to record DEVICE_UNTRUSTED audit event: ${errorMessage}`, {
1711
2227
  error: auditError,
@@ -1716,11 +2232,15 @@ class AuthService {
1716
2232
  }
1717
2233
  }
1718
2234
  catch (error) {
2235
+ // Non-blocking: Log but continue
1719
2236
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1720
2237
  this.logger?.debug?.(`Failed to revoke trusted device token on logout: ${errorMessage}`, { error });
1721
2238
  }
1722
2239
  }
1723
2240
  }
2241
+ // ============================================================================
2242
+ // Automatically Clear Auth Cookies (if using cookie-based token delivery)
2243
+ // ============================================================================
1724
2244
  const response = this.clientInfoService.getResponse();
1725
2245
  if (response && this.config.tokenDelivery?.method !== 'json') {
1726
2246
  this.clearAuthCookies(response, dto.forgetMe ?? false);
@@ -1728,30 +2248,49 @@ class AuthService {
1728
2248
  }
1729
2249
  return { success: true };
1730
2250
  }
2251
+ /**
2252
+ * Clear authentication cookies from response
2253
+ *
2254
+ * @param response - HTTP response object with clearCookie method
2255
+ * @param forgetDevice - Whether to also clear device token cookie
2256
+ * @private
2257
+ */
1731
2258
  clearAuthCookies(response, forgetDevice) {
1732
2259
  if (!response.clearCookie) {
1733
- return;
2260
+ return; // Response doesn't support cookie clearing (shouldn't happen)
1734
2261
  }
1735
2262
  const cookieOptions = this.config.tokenDelivery?.cookieOptions || {};
1736
2263
  const prefix = this.config.tokenDelivery?.cookieNamePrefix || 'nauth';
2264
+ // Clear access and refresh tokens
1737
2265
  response.clearCookie(`${prefix}_access_token`, cookieOptions);
1738
2266
  response.clearCookie(`${prefix}_refresh_token`, cookieOptions);
2267
+ // Clear CSRF token cookie (httpOnly: false, so it can be cleared)
2268
+ // Use the same cookie options but with httpOnly: false to match how it was set
1739
2269
  const csrfCookieOptions = {
1740
2270
  ...cookieOptions,
1741
- httpOnly: false,
2271
+ httpOnly: false, // CSRF token cookie is not httpOnly
1742
2272
  };
1743
2273
  const csrfCookieName = this.config.security?.csrf?.cookieName || `${prefix}_csrf_token`;
1744
2274
  response.clearCookie(csrfCookieName, csrfCookieOptions);
2275
+ // Clear device token if forgetting device
1745
2276
  if (forgetDevice) {
1746
2277
  response.clearCookie(`${prefix}_device_token`, cookieOptions);
1747
2278
  }
1748
2279
  }
2280
+ /**
2281
+ * Global signout (revoke all user sessions)
2282
+ * @param sub - External user identifier (sub/UUID)
2283
+ * @returns Number of sessions revoked
2284
+ */
1749
2285
  async logoutAll(dto) {
2286
+ // Get user by sub to get internal id
1750
2287
  const user = (await this.userRepository.findOne({ where: { sub: dto.sub } }));
1751
2288
  if (!user) {
1752
2289
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
1753
2290
  }
2291
+ // Use internal id for session queries
1754
2292
  const revokedCount = await this.sessionService.revokeAllUserSessions(user.id, 'Global signout');
2293
+ // Revoke all trusted devices if forgetDevices flag is set
1755
2294
  let revokedDevicesCount = 0;
1756
2295
  let revokedDevices = [];
1757
2296
  if (dto.forgetDevices &&
@@ -1763,6 +2302,7 @@ class AuthService {
1763
2302
  revokedDevicesCount = deviceRevocationResult.revokedCount;
1764
2303
  revokedDevices = deviceRevocationResult.devices;
1765
2304
  this.logger?.log?.(`Revoked ${revokedDevicesCount} trusted device(s) for user ${user.sub} (forgetDevices=true)`);
2305
+ // Record audit event for device revocation
1766
2306
  if (revokedDevicesCount > 0 && this.auditService) {
1767
2307
  try {
1768
2308
  const userId = typeof user.id === 'number' ? user.id : parseInt(String(user.id), 10);
@@ -1784,6 +2324,7 @@ class AuthService {
1784
2324
  });
1785
2325
  }
1786
2326
  catch (auditError) {
2327
+ // Non-blocking: Log but continue
1787
2328
  const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
1788
2329
  this.logger?.error?.(`Failed to record DEVICE_UNTRUSTED audit event: ${errorMessage}`, {
1789
2330
  error: auditError,
@@ -1793,10 +2334,14 @@ class AuthService {
1793
2334
  }
1794
2335
  }
1795
2336
  catch (error) {
2337
+ // Non-blocking: Log but continue
1796
2338
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1797
2339
  this.logger?.debug?.(`Failed to revoke trusted devices on global logout: ${errorMessage}`, { error });
1798
2340
  }
1799
2341
  }
2342
+ // ============================================================================
2343
+ // Audit: Record GLOBAL_SIGNOUT event (individual SESSION_REVOKED events recorded in SessionService)
2344
+ // ============================================================================
1800
2345
  if (this.auditService && revokedCount > 0) {
1801
2346
  try {
1802
2347
  const userId = typeof user.id === 'number' ? user.id : parseInt(String(user.id), 10);
@@ -1827,6 +2372,7 @@ class AuthService {
1827
2372
  });
1828
2373
  }
1829
2374
  catch (auditError) {
2375
+ // Non-blocking: Log but continue (individual SESSION_REVOKED events already recorded in SessionService)
1830
2376
  const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
1831
2377
  this.logger?.error?.(`Failed to record GLOBAL_SIGNOUT audit event: ${errorMessage}`, {
1832
2378
  error: auditError,
@@ -1834,28 +2380,60 @@ class AuthService {
1834
2380
  });
1835
2381
  }
1836
2382
  }
2383
+ // ============================================================================
2384
+ // Automatically Clear Auth Cookies (if using cookie-based token delivery)
2385
+ // ============================================================================
1837
2386
  const response = this.clientInfoService.getResponse();
1838
2387
  if (response && this.config.tokenDelivery?.method !== 'json') {
2388
+ // Clear auth cookies
2389
+ // If forgetDevices is true, also clear device token cookie
1839
2390
  this.clearAuthCookies(response, dto.forgetDevices ?? false);
1840
2391
  this.logger?.debug?.('Auth cookies cleared automatically on global logout');
1841
2392
  }
1842
2393
  return { revokedCount };
1843
2394
  }
2395
+ // ============================================================================
2396
+ // Password Management
2397
+ // ============================================================================
2398
+ /**
2399
+ * Change the password for an existing user.
2400
+ *
2401
+ * Verifies the current password, validates the new password,
2402
+ * checks password reuse policy, and updates the user's password hash and history.
2403
+ * Executes configured pre-change hooks if provided.
2404
+ *
2405
+ * @param sub - External user identifier (sub/UUID)
2406
+ * @param dto - ChangePasswordDTO containing old and new password
2407
+ * @returns void
2408
+ * @throws {NAuthException} If the user is not found, current password is incorrect, the new password is weak, password reuse is detected, or password change is disallowed by hooks.
2409
+ *
2410
+ * @example
2411
+ * ```typescript
2412
+ * await authService.changePassword('user-uuid', {
2413
+ * oldPassword: 'currentPass123!',
2414
+ * newPassword: 'newStr0ngPass!@#',
2415
+ * });
2416
+ * ```
2417
+ */
1844
2418
  async changePassword(dto) {
2419
+ // Get user by sub
1845
2420
  const user = (await this.userRepository.findOne({ where: { sub: dto.sub } }));
1846
2421
  if (!user || !user.passwordHash) {
1847
2422
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
1848
2423
  }
2424
+ // Execute beforePasswordChange hook (use sub for external API)
1849
2425
  if (this.config.hooks?.beforePasswordChange) {
1850
2426
  const result = await this.config.hooks.beforePasswordChange(dto.sub, dto.oldPassword);
1851
2427
  if (result === false) {
1852
2428
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_CHANGE_NOT_ALLOWED, 'Password change not allowed');
1853
2429
  }
1854
2430
  }
2431
+ // Verify old password
1855
2432
  const isValid = await this.passwordService.verifyPassword(dto.oldPassword, user.passwordHash);
1856
2433
  if (!isValid) {
1857
2434
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_INCORRECT, 'Current password is incorrect');
1858
2435
  }
2436
+ // Execute afterPasswordChange hook (use sub for external API)
1859
2437
  if (this.config.hooks?.afterPasswordChange) {
1860
2438
  await this.config.hooks.afterPasswordChange(dto.sub);
1861
2439
  }
@@ -1872,13 +2450,30 @@ class AuthService {
1872
2450
  });
1873
2451
  return { success: true };
1874
2452
  }
2453
+ /**
2454
+ * Update user profile attributes.
2455
+ *
2456
+ * Updates user fields (name, email, phone, username, metadata) and enforces unique constraints and verification rules.
2457
+ *
2458
+ * @param sub - User sub/UUID
2459
+ * @param updateData - User fields to update
2460
+ * @returns Updated user object
2461
+ * @throws {NAuthException} If user not found or unique constraint violated
2462
+ *
2463
+ * @example
2464
+ * await authService.updateUserAttributes(sub, { email: 'test@example.com' });
2465
+ */
1875
2466
  async updateUserAttributes(dto) {
2467
+ // Find user by sub (external identifier)
1876
2468
  const user = (await this.userRepository.findOne({ where: { sub: dto.sub } }));
1877
2469
  if (!user) {
1878
2470
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
1879
2471
  }
2472
+ // Check for uniqueness constraints - use internal id
1880
2473
  await this.validateUniquenessConstraints(user.id, dto);
2474
+ // Prepare update object
1881
2475
  const updateFields = {};
2476
+ // Update basic fields if provided
1882
2477
  if (dto.firstName !== undefined) {
1883
2478
  updateFields.firstName = dto.firstName;
1884
2479
  }
@@ -1891,15 +2486,25 @@ class AuthService {
1891
2486
  if (dto.email !== undefined) {
1892
2487
  const oldEmail = user.email;
1893
2488
  updateFields.email = dto.email;
2489
+ // Reset email verification if email changed (unless retainVerification is true)
1894
2490
  if (dto.email !== user.email) {
1895
2491
  if (!dto.retainVerification) {
1896
2492
  updateFields.isEmailVerified = false;
1897
2493
  }
1898
2494
  else {
2495
+ // Explicitly retain current verification status
1899
2496
  updateFields.isEmailVerified = user.isEmailVerified;
1900
2497
  }
2498
+ // ============================================================================
2499
+ // MFA Device Management: Handle Email MFA devices when email changes
2500
+ // ============================================================================
2501
+ // When email address changes, Email MFA devices become invalid.
2502
+ // We deactivate them and check if user has any other active MFA devices.
2503
+ // If Email was the only MFA method, user will need to set up MFA again.
2504
+ // This happens automatically via challenge system at next login.
1901
2505
  if (oldEmail && this.mfaDeviceRepository) {
1902
2506
  try {
2507
+ // Find all Email MFA devices (email field may be null in legacy devices)
1903
2508
  const emailDevices = (await this.mfaDeviceRepository.find({
1904
2509
  where: {
1905
2510
  userId: user.id,
@@ -1909,10 +2514,12 @@ class AuthService {
1909
2514
  }));
1910
2515
  if (emailDevices.length > 0) {
1911
2516
  this.logger?.log?.(`Deleting ${emailDevices.length} Email MFA device(s) for user ${user.sub} due to email address change (old: ${oldEmail}, new: ${dto.email})`);
2517
+ // Delete all Email devices (can't be reactivated with old email)
1912
2518
  for (const device of emailDevices) {
1913
2519
  const deviceId = device.id;
1914
2520
  await this.mfaDeviceRepository.delete(deviceId);
1915
2521
  }
2522
+ // Record audit event for removed Email MFA devices
1916
2523
  if (this.auditService) {
1917
2524
  try {
1918
2525
  await this.auditService.recordEvent({
@@ -1935,12 +2542,14 @@ class AuthService {
1935
2542
  this.logger?.error?.(`Failed to record MFA_DEVICE_REMOVED audit event for email change: ${errorMessage}`, { error: auditError, userId: user.id });
1936
2543
  }
1937
2544
  }
2545
+ // Check if user has any other active MFA devices
1938
2546
  const allActiveDevices = (await this.mfaDeviceRepository.find({
1939
2547
  where: {
1940
2548
  userId: user.id,
1941
2549
  isActive: true,
1942
2550
  },
1943
2551
  }));
2552
+ // If no active devices remain and user had MFA enabled, disable MFA
1944
2553
  if (allActiveDevices.length === 0 && user.mfaEnabled) {
1945
2554
  updateFields.mfaEnabled = false;
1946
2555
  updateFields.mfaMethods = [];
@@ -1953,6 +2562,8 @@ class AuthService {
1953
2562
  }
1954
2563
  }
1955
2564
  catch (error) {
2565
+ // Log error but don't fail the email update
2566
+ // This handles cases where MFA module is not imported (mfaDeviceRepository might not be available)
1956
2567
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1957
2568
  this.logger?.warn?.(`Failed to handle MFA device deactivation during email change for user ${user.sub}: ${errorMessage}`);
1958
2569
  }
@@ -1962,15 +2573,25 @@ class AuthService {
1962
2573
  if (dto.phone !== undefined) {
1963
2574
  const oldPhone = user.phone;
1964
2575
  updateFields.phone = dto.phone;
2576
+ // Reset phone verification if phone changed (unless retainVerification is true)
1965
2577
  if (dto.phone !== user.phone) {
1966
2578
  if (!dto.retainVerification) {
1967
2579
  updateFields.isPhoneVerified = false;
1968
2580
  }
1969
2581
  else {
2582
+ // Explicitly retain current verification status
1970
2583
  updateFields.isPhoneVerified = user.isPhoneVerified;
1971
2584
  }
2585
+ // ============================================================================
2586
+ // MFA Device Management: Handle SMS MFA devices when phone changes
2587
+ // ============================================================================
2588
+ // When phone number changes, SMS MFA devices become invalid.
2589
+ // We delete them and check if user has any other active MFA devices.
2590
+ // If SMS was the only MFA method, user will need to set up MFA again.
2591
+ // This happens automatically via challenge system at next login.
1972
2592
  if (oldPhone && this.mfaDeviceRepository) {
1973
2593
  try {
2594
+ // Find all SMS MFA devices (SMS MFA is tied to user.phone, not device phoneNumber)
1974
2595
  const smsDevices = (await this.mfaDeviceRepository.find({
1975
2596
  where: {
1976
2597
  userId: user.id,
@@ -1980,10 +2601,12 @@ class AuthService {
1980
2601
  }));
1981
2602
  if (smsDevices.length > 0) {
1982
2603
  this.logger?.log?.(`Deleting ${smsDevices.length} SMS MFA device(s) for user ${user.sub} due to phone number change (old: ${oldPhone}, new: ${dto.phone})`);
2604
+ // Delete all SMS devices (can't be reactivated with old phone number)
1983
2605
  for (const device of smsDevices) {
1984
2606
  const deviceId = device.id;
1985
2607
  await this.mfaDeviceRepository.delete(deviceId);
1986
2608
  }
2609
+ // Record audit event for removed SMS MFA devices
1987
2610
  if (this.auditService) {
1988
2611
  try {
1989
2612
  await this.auditService.recordEvent({
@@ -2006,12 +2629,14 @@ class AuthService {
2006
2629
  this.logger?.error?.(`Failed to record MFA_DEVICE_REMOVED audit event for phone change: ${errorMessage}`, { error: auditError, userId: user.id });
2007
2630
  }
2008
2631
  }
2632
+ // Check if user has any other active MFA devices
2009
2633
  const allActiveDevices = (await this.mfaDeviceRepository.find({
2010
2634
  where: {
2011
2635
  userId: user.id,
2012
2636
  isActive: true,
2013
2637
  },
2014
2638
  }));
2639
+ // If no active devices remain and user had MFA enabled, disable MFA
2015
2640
  if (allActiveDevices.length === 0 && user.mfaEnabled) {
2016
2641
  updateFields.mfaEnabled = false;
2017
2642
  updateFields.mfaMethods = [];
@@ -2024,27 +2649,40 @@ class AuthService {
2024
2649
  }
2025
2650
  }
2026
2651
  catch (error) {
2652
+ // Log error but don't fail the phone update
2653
+ // This handles cases where MFA module is not imported (mfaDeviceRepository might not be available)
2027
2654
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2028
2655
  this.logger?.warn?.(`Failed to handle MFA device deactivation during phone change for user ${user.sub}: ${errorMessage}`);
2029
2656
  }
2030
2657
  }
2031
2658
  }
2032
2659
  }
2660
+ // Handle preferred MFA method
2033
2661
  if (dto.preferredMfaMethod !== undefined) {
2034
2662
  updateFields.preferredMfaMethod = dto.preferredMfaMethod;
2035
2663
  }
2664
+ // Handle metadata merge
2036
2665
  if (dto.metadata !== undefined) {
2037
2666
  const existingMetadata = user.metadata || {};
2038
2667
  updateFields.metadata = { ...existingMetadata, ...dto.metadata };
2039
2668
  }
2669
+ // Update user in database - use internal id for update query
2040
2670
  await this.userRepository.update(user.id, updateFields);
2671
+ // Fetch updated user - use internal id
2041
2672
  const updatedUser = (await this.userRepository.findOne({ where: { id: user.id } }));
2042
2673
  if (!updatedUser) {
2043
2674
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found after update');
2044
2675
  }
2676
+ // ============================================================================
2677
+ // Audit: Record profile and attribute updates
2678
+ // ============================================================================
2045
2679
  try {
2680
+ // Client info (ipAddress, userAgent) automatically extracted from ClientInfoService
2681
+ // Note: ClientInfoService is used transparently by SessionService and AuditService
2046
2682
  const updatedFieldNames = Object.keys(updateFields);
2683
+ // Build field changes map with before/after values
2047
2684
  const fieldChanges = {};
2685
+ // Capture before/after values for each updated field
2048
2686
  if (dto.firstName !== undefined && dto.firstName !== user.firstName) {
2049
2687
  fieldChanges.firstName = {
2050
2688
  before: user.firstName ?? null,
@@ -2063,6 +2701,8 @@ class AuthService {
2063
2701
  after: dto.username ?? null,
2064
2702
  };
2065
2703
  }
2704
+ // Note: email and phone are tracked separately with specific audit events,
2705
+ // but we include them in fieldChanges for completeness
2066
2706
  if (dto.email !== undefined && dto.email !== user.email) {
2067
2707
  fieldChanges.email = {
2068
2708
  before: user.email ?? null,
@@ -2081,14 +2721,17 @@ class AuthService {
2081
2721
  after: dto.preferredMfaMethod ?? null,
2082
2722
  };
2083
2723
  }
2724
+ // Handle metadata changes (merged, so track what was added/changed)
2084
2725
  if (dto.metadata !== undefined) {
2085
2726
  const oldMetadata = user.metadata || {};
2086
2727
  const newMetadata = { ...oldMetadata, ...dto.metadata };
2087
2728
  const metadataChanges = {};
2729
+ // Track all keys in new metadata
2088
2730
  const allKeys = new Set([...Object.keys(oldMetadata), ...Object.keys(dto.metadata)]);
2089
2731
  for (const key of allKeys) {
2090
2732
  const oldValue = oldMetadata[key];
2091
2733
  const newValue = newMetadata[key];
2734
+ // Only track if value actually changed
2092
2735
  if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
2093
2736
  metadataChanges[key] = {
2094
2737
  before: oldValue ?? null,
@@ -2100,6 +2743,7 @@ class AuthService {
2100
2743
  fieldChanges.metadata = metadataChanges;
2101
2744
  }
2102
2745
  }
2746
+ // Track verification status changes if email/phone changed
2103
2747
  if (dto.email !== undefined && dto.email !== user.email) {
2104
2748
  const emailVerificationChanged = !dto.retainVerification && updateFields.isEmailVerified === false;
2105
2749
  if (emailVerificationChanged) {
@@ -2118,21 +2762,25 @@ class AuthService {
2118
2762
  };
2119
2763
  }
2120
2764
  }
2765
+ // Record general profile update with field changes
2121
2766
  await this.auditService?.recordEvent({
2122
2767
  userId: user.id,
2123
2768
  eventType: auth_audit_event_type_enum_1.AuthAuditEventType.PROFILE_UPDATED,
2124
2769
  eventStatus: 'INFO',
2125
2770
  metadata: {
2771
+ // Client info automatically included from context
2126
2772
  updatedFields: updatedFieldNames,
2127
2773
  fieldChanges: Object.keys(fieldChanges).length > 0 ? fieldChanges : undefined,
2128
2774
  },
2129
2775
  });
2776
+ // Record specific field changes
2130
2777
  if (dto.email !== undefined && dto.email !== user.email) {
2131
2778
  await this.auditService?.recordEvent({
2132
2779
  userId: user.id,
2133
2780
  eventType: auth_audit_event_type_enum_1.AuthAuditEventType.EMAIL_CHANGED,
2134
2781
  eventStatus: 'INFO',
2135
2782
  metadata: {
2783
+ // Client info automatically included from context
2136
2784
  oldEmail: user.email,
2137
2785
  newEmail: dto.email,
2138
2786
  retainVerification: dto.retainVerification || false,
@@ -2145,6 +2793,7 @@ class AuthService {
2145
2793
  eventType: auth_audit_event_type_enum_1.AuthAuditEventType.PHONE_CHANGED,
2146
2794
  eventStatus: 'INFO',
2147
2795
  metadata: {
2796
+ // Client info automatically included from context
2148
2797
  oldPhone: user.phone,
2149
2798
  newPhone: dto.phone,
2150
2799
  retainVerification: dto.retainVerification || false,
@@ -2157,6 +2806,7 @@ class AuthService {
2157
2806
  eventType: auth_audit_event_type_enum_1.AuthAuditEventType.USERNAME_CHANGED,
2158
2807
  eventStatus: 'INFO',
2159
2808
  metadata: {
2809
+ // Client info automatically included from context
2160
2810
  oldUsername: user.username,
2161
2811
  newUsername: dto.username,
2162
2812
  },
@@ -2164,16 +2814,33 @@ class AuthService {
2164
2814
  }
2165
2815
  }
2166
2816
  catch (auditError) {
2817
+ // Non-blocking: Log but continue
2167
2818
  const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
2168
2819
  this.logger?.error?.(`Failed to record profile update audit events: ${errorMessage}`, {
2169
2820
  error: auditError,
2170
2821
  userId: user.id,
2171
2822
  });
2172
2823
  }
2824
+ // Return user response DTO
2173
2825
  return user_response_dto_1.UserResponseDto.fromEntity(updatedUser);
2174
2826
  }
2827
+ /**
2828
+ * Ensures email, phone, and username are unique for other users before update.
2829
+ *
2830
+ * Throws if another user already has the specified email, phone, or username.
2831
+ *
2832
+ * @param userId - Internal numeric user ID (excluded from check)
2833
+ * @param updateData - User fields to check for uniqueness
2834
+ * @throws {NAuthException} If a unique constraint is violated for email, phone, or username
2835
+ *
2836
+ * @example
2837
+ * ```typescript
2838
+ * await authService.validateUniquenessConstraints(1, { email: "test@example.com" });
2839
+ * ```
2840
+ */
2175
2841
  async validateUniquenessConstraints(userId, updateData) {
2176
2842
  const conflicts = [];
2843
+ // Check email uniqueness
2177
2844
  if (updateData.email) {
2178
2845
  const existingUser = await this.userRepository.findOne({
2179
2846
  where: { email: updateData.email },
@@ -2182,6 +2849,7 @@ class AuthService {
2182
2849
  conflicts.push('Email already exists');
2183
2850
  }
2184
2851
  }
2852
+ // Check phone uniqueness
2185
2853
  if (updateData.phone) {
2186
2854
  const existingUser = await this.userRepository.findOne({
2187
2855
  where: { phone: updateData.phone },
@@ -2190,6 +2858,7 @@ class AuthService {
2190
2858
  conflicts.push('Phone number already exists');
2191
2859
  }
2192
2860
  }
2861
+ // Check username uniqueness
2193
2862
  if (updateData.username) {
2194
2863
  const existingUser = await this.userRepository.findOne({
2195
2864
  where: { username: updateData.username },
@@ -2204,9 +2873,34 @@ class AuthService {
2204
2873
  });
2205
2874
  }
2206
2875
  }
2876
+ // ============================================================================
2877
+ // Helper Methods
2878
+ // ============================================================================
2879
+ /**
2880
+ * Checks if the login identifier matches the specified allowed type.
2881
+ *
2882
+ * Determines if the given identifier is a valid email, username, phone, or allowed hybrid,
2883
+ * according to the configured identifier type restriction.
2884
+ *
2885
+ * @param identifier - The login identifier to check (email, username, or phone)
2886
+ * @param allowedType - The permitted identifier type ('email', 'username', 'phone', or 'email_or_username')
2887
+ * @returns True if the identifier conforms to the allowed type, otherwise false
2888
+ *
2889
+ * @example
2890
+ * ```typescript
2891
+ * // Email check
2892
+ * const valid = this.validateIdentifierType('user@example.com', 'email'); // true
2893
+ *
2894
+ * // Username check
2895
+ * const valid = this.validateIdentifierType('johndoe', 'username'); // true
2896
+ * ```
2897
+ */
2207
2898
  validateIdentifierType(identifier, allowedType) {
2899
+ // Check if identifier is an email (contains @)
2208
2900
  const isEmail = identifier.includes('@');
2901
+ // Check if identifier looks like a phone (starts with + and contains digits)
2209
2902
  const isPhone = /^\+[1-9]\d{1,14}$/.test(identifier.trim());
2903
+ // If not email or phone, assume it's a username
2210
2904
  const isUsername = !isEmail && !isPhone;
2211
2905
  switch (allowedType) {
2212
2906
  case 'email':
@@ -2218,18 +2912,37 @@ class AuthService {
2218
2912
  case 'email_or_username':
2219
2913
  return isEmail || isUsername;
2220
2914
  default:
2221
- return true;
2915
+ return true; // No restriction
2222
2916
  }
2223
2917
  }
2918
+ /**
2919
+ * Retrieves a user entity by login identifier.
2920
+ *
2921
+ * Performs a lookup for a user by email, username, or phone number.
2922
+ * The search respects the identifierType restriction when provided, limiting which fields are queried.
2923
+ *
2924
+ * @param identifier - Login credential (email, username, or phone)
2925
+ * @param identifierType - Restricts search to a specific identifier type ('email', 'username', 'phone', or 'email_or_username')
2926
+ * @returns The user entity if found, otherwise null
2927
+ *
2928
+ * @example
2929
+ * ```typescript
2930
+ * const user = await this.findUserByIdentifier('user@example.com');
2931
+ * const user2 = await this.findUserByIdentifier('johndoe', 'username');
2932
+ * ```
2933
+ */
2224
2934
  async findUserByIdentifier(identifier, identifierType) {
2225
2935
  const queryBuilder = this.userRepository.createQueryBuilder('user');
2936
+ // Build query based on identifier type restriction
2226
2937
  if (!identifierType) {
2938
+ // No restriction - search all fields
2227
2939
  queryBuilder
2228
2940
  .where('user.email = :identifier', { identifier })
2229
2941
  .orWhere('user.username = :identifier', { identifier })
2230
2942
  .orWhere('user.phone = :identifier', { identifier });
2231
2943
  }
2232
2944
  else {
2945
+ // Apply restriction based on identifier type
2233
2946
  switch (identifierType) {
2234
2947
  case 'email':
2235
2948
  queryBuilder.where('user.email = :identifier', { identifier });
@@ -2247,6 +2960,7 @@ class AuthService {
2247
2960
  break;
2248
2961
  }
2249
2962
  }
2963
+ // Select only columns required for login checks and response shaping to reduce row size
2250
2964
  queryBuilder.select([
2251
2965
  'user.id',
2252
2966
  'user.sub',
@@ -2263,28 +2977,65 @@ class AuthService {
2263
2977
  'user.preferredMfaMethod',
2264
2978
  'user.isEmailVerified',
2265
2979
  'user.isPhoneVerified',
2266
- 'user.mfaExempt',
2980
+ 'user.mfaExempt', // Required for MFA exemption check in challenge flow
2981
+ // The following are used for messaging/challenge determination when needed
2267
2982
  'user.socialProviders',
2268
2983
  'user.backupCodes',
2269
2984
  ]);
2270
2985
  return (await queryBuilder.getOne());
2271
2986
  }
2987
+ /**
2988
+ * Handles a failed login by recording the attempt, applying IP-based lockout policy,
2989
+ * and invoking relevant hooks.
2990
+ *
2991
+ * @param identifier - User identifier (email/username/phone)
2992
+ * @param reason - Optional reason for failure
2993
+ * @returns Promise<void>
2994
+ *
2995
+ * @example
2996
+ * ```typescript
2997
+ * await authService.handleFailedLogin('user@example.com', 'invalid_credentials');
2998
+ * ```
2999
+ */
2272
3000
  async handleFailedLogin(identifier, reason) {
3001
+ // Get client IP address for lockout tracking
2273
3002
  const clientInfo = this.clientInfoService.get();
2274
3003
  const ipAddress = clientInfo.ipAddress;
3004
+ // Record failed attempt
2275
3005
  await this.recordLoginAttempt(identifier, false, reason);
3006
+ // Increment IP-based lockout counter if enabled
2276
3007
  if (this.config.lockout?.enabled && ipAddress) {
2277
3008
  const attempts = await this.accountLockoutStorage.recordFailedAttempt(ipAddress);
3009
+ // Lock IP if max attempts reached
2278
3010
  if (attempts >= (this.config.lockout.maxAttempts || 5)) {
2279
- await this.accountLockoutStorage.blockIpAdresss(ipAddress, this.config.lockout.duration || 900, 'Too many failed login attempts from this IP');
2280
- }
2281
- }
3011
+ await this.accountLockoutStorage.blockIpAdresss(ipAddress, this.config.lockout.duration || 900, // 15 minutes default
3012
+ 'Too many failed login attempts from this IP');
3013
+ // // Execute hook with IP address
3014
+ // if (this.config.hooks?.afterAccountLock) {
3015
+ // await this.config.hooks.afterAccountLock(identifier, 'Too many failed attempts from IP', clientInfo);
3016
+ // }
3017
+ }
3018
+ }
3019
+ // // Execute hook
3020
+ // if (this.config.hooks?.afterLoginFailed) {
3021
+ // await this.config.hooks.afterLoginFailed(identifier, reason || 'unknown');
3022
+ // }
2282
3023
  }
3024
+ /**
3025
+ * Records a login attempt with client context.
3026
+ *
3027
+ * @param email - User's email address
3028
+ * @param success - True if login succeeded, false if failed
3029
+ * @param failureReason - Optional reason for failure
3030
+ * @param userId - Optional internal user ID (only for successful logins)
3031
+ * @returns Promise<void>
3032
+ */
2283
3033
  async recordLoginAttempt(email, success, failureReason, userId) {
3034
+ // Get client info from context
2284
3035
  const clientInfo = this.clientInfoService.get();
2285
3036
  const attempt = this.loginAttemptRepository.create({
2286
3037
  email,
2287
- userId,
3038
+ userId, // Internal user ID (integer)
2288
3039
  ipAddress: clientInfo.ipAddress,
2289
3040
  userAgent: clientInfo.userAgent,
2290
3041
  success,
@@ -2292,10 +3043,28 @@ class AuthService {
2292
3043
  });
2293
3044
  await this.loginAttemptRepository.save(attempt);
2294
3045
  }
3046
+ /**
3047
+ * Get user by ID (sub)
3048
+ * @param sub - User sub (external identifier)
3049
+ * @returns User entity or null
3050
+ */
2295
3051
  async getUserById(dto) {
2296
3052
  const user = (await this.userRepository.findOne({ where: { sub: dto.sub } }));
2297
3053
  return user ? user_response_dto_1.UserResponseDto.fromEntity(user) : null;
2298
3054
  }
3055
+ /**
3056
+ * Get user by email address.
3057
+ *
3058
+ * @param email - User email
3059
+ * @param requireEmailVerified - Only return user if email is verified (default: false)
3060
+ * @returns User entity or null
3061
+ * @internal - For use by social auth providers
3062
+ *
3063
+ * @example
3064
+ * ```typescript
3065
+ * const user = await authService.getUserByEmail('user@example.com', true);
3066
+ * ```
3067
+ */
2299
3068
  async getUserByEmail(dto) {
2300
3069
  const where = dto.requireEmailVerified
2301
3070
  ? { email: dto.email, isEmailVerified: true }
@@ -2303,11 +3072,25 @@ class AuthService {
2303
3072
  const user = (await this.userRepository.findOne({ where }));
2304
3073
  return user ? user_response_dto_1.UserResponseDto.fromEntity(user) : null;
2305
3074
  }
3075
+ /**
3076
+ * Require user to change password at next login.
3077
+ *
3078
+ * Throws if user not found or has no password set (e.g. social login only).
3079
+ *
3080
+ * @param userId - User's sub identifier
3081
+ * @returns Resolves when flag is set
3082
+ * @throws {NAuthException} If user is not found or cannot change password
3083
+ *
3084
+ * @example
3085
+ * await authService.setMustChangePassword('user-uuid-123');
3086
+ */
2306
3087
  async setMustChangePassword(dto) {
2307
3088
  const user = await this.userRepository.findOne({ where: { sub: dto.userId } });
2308
3089
  if (!user) {
2309
3090
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
2310
3091
  }
3092
+ // CRITICAL PROTECTION: Only allow for users with password authentication
3093
+ // Pure social users cannot be forced to change password
2311
3094
  if (!user.passwordHash) {
2312
3095
  this.logger?.warn?.(`Cannot force password change for user ${dto.userId} - user doesn't have a password (pure social signup)`);
2313
3096
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_CHANGE_NOT_ALLOWED, 'Password change not available. This account uses social authentication only and has no password.');
@@ -2316,14 +3099,53 @@ class AuthService {
2316
3099
  this.logger?.log?.(`Must-change-password flag set for user: ${dto.userId}`);
2317
3100
  return { success: true };
2318
3101
  }
3102
+ /**
3103
+ * Admin-only: Reset a user's password by identifier.
3104
+ *
3105
+ * Allows administrators to reset a user's password using any identifier
3106
+ * (email, username, phone, or sub). Automatically revokes sessions and optionally
3107
+ * requires password change on next login using the existing challenge system.
3108
+ *
3109
+ * SECURITY: This is an admin-only operation. Ensure proper authorization
3110
+ * checks are in place before calling this method.
3111
+ *
3112
+ * @param dto - Admin reset password request
3113
+ * @returns Response with success status and session revocation count
3114
+ * @throws {NAuthException} If user not found, user has no password (social-only), or password validation fails
3115
+ *
3116
+ * @example
3117
+ * ```typescript
3118
+ * // Reset with force password change
3119
+ * const result = await authService.adminSetPassword({
3120
+ * identifier: 'user@example.com',
3121
+ * newPassword: 'NewSecurePassword123!',
3122
+ * mustChangePassword: true,
3123
+ * revokeSessions: true
3124
+ * });
3125
+ *
3126
+ * // Reset without forcing password change
3127
+ * const result = await authService.adminSetPassword({
3128
+ * identifier: 'a21b654c-2746-4168-acee-c175083a65cd',
3129
+ * newPassword: 'NewSecurePassword123!',
3130
+ * mustChangePassword: false
3131
+ * });
3132
+ * ```
3133
+ */
2319
3134
  async adminSetPassword(dto) {
2320
3135
  this.logger?.log?.(`Admin password reset requested for identifier: ${dto.identifier}`);
2321
3136
  this.logger?.debug?.(`Reset details: { identifier: ${dto.identifier}, mustChangePassword: ${dto.mustChangePassword ?? true}, revokeSessions: ${dto.revokeSessions ?? true} }`);
3137
+ // ============================================================================
3138
+ // Find User by Identifier
3139
+ // ============================================================================
3140
+ // Support multiple identifier types: email, username, phone, or sub (UUID)
2322
3141
  let user = null;
3142
+ // Try to find by sub (UUID) first if it looks like a UUID.
3143
+ // WHY: Many deployments treat `sub` as the primary immutable identifier.
2323
3144
  if ((0, class_validator_1.isUUID)(dto.identifier)) {
2324
3145
  this.logger?.debug?.(`Identifier appears to be UUID, searching by sub: ${dto.identifier}`);
2325
3146
  user = (await this.userRepository.findOne({ where: { sub: dto.identifier } }));
2326
3147
  }
3148
+ // If not found by sub, try by identifier (email, username, phone)
2327
3149
  if (!user) {
2328
3150
  this.logger?.debug?.(`Searching by identifier (email/username/phone): ${dto.identifier}`);
2329
3151
  user = await this.findUserByIdentifier(dto.identifier);
@@ -2332,11 +3154,16 @@ class AuthService {
2332
3154
  this.logger?.warn?.(`Password reset failed - user not found: ${dto.identifier}`);
2333
3155
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
2334
3156
  }
3157
+ // ============================================================================
3158
+ // Validate User Can Have Password Reset
3159
+ // ============================================================================
3160
+ // CRITICAL PROTECTION: Only allow for users with password authentication
3161
+ // Pure social users cannot have password reset (they don't have passwords)
2335
3162
  if (!user.passwordHash) {
2336
3163
  this.logger?.warn?.(`Password reset failed - user doesn't have a password (pure social signup): ${user.sub}`);
2337
3164
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_CHANGE_NOT_ALLOWED, 'Password reset not available. This account uses social authentication only and has no password.');
2338
3165
  }
2339
- const mustChangePassword = dto.mustChangePassword ?? true;
3166
+ const mustChangePassword = dto.mustChangePassword ?? true; // Default to true for security
2340
3167
  const revokeSessions = dto.revokeSessions !== false;
2341
3168
  const { sessionsRevoked } = await this.updateUserPassword({
2342
3169
  user,
@@ -2355,27 +3182,59 @@ class AuthService {
2355
3182
  },
2356
3183
  },
2357
3184
  });
3185
+ // ============================================================================
3186
+ // Return Response
3187
+ // ============================================================================
2358
3188
  return {
2359
3189
  success: true,
2360
3190
  mustChangePassword,
2361
3191
  sessionsRevoked,
2362
3192
  };
2363
3193
  }
3194
+ // ============================================================================
3195
+ // Forgot Password (Account Recovery)
3196
+ // ============================================================================
3197
+ /**
3198
+ * Request a password reset code for an account.
3199
+ *
3200
+ * Security:
3201
+ * - Avoids account enumeration: returns success even when user is not found.
3202
+ * - Delivery is best-effort; errors are logged but should not reveal account existence.
3203
+ *
3204
+ * Channel selection (per config.signup.verificationMethod):
3205
+ * - 'none': send to email if available; else phone (if available)
3206
+ * - 'email': only send to verified email
3207
+ * - 'phone': only send to verified phone
3208
+ * - 'both': prefer verified email; fallback to verified phone
3209
+ *
3210
+ * @param dto - Forgot password request payload
3211
+ * @returns Delivery metadata (masked destination) when available
3212
+ */
2364
3213
  async forgotPassword(dto) {
2365
3214
  const response = { success: true };
2366
3215
  if (!this.passwordResetService) {
3216
+ // Do not leak configuration details to clients.
2367
3217
  this.logger?.warn?.('PasswordResetService not configured; forgotPassword will not send any delivery');
2368
3218
  return response;
2369
3219
  }
3220
+ // Respect identifier type restrictions (if configured)
2370
3221
  if (this.config.login?.identifierType &&
2371
3222
  !this.validateIdentifierType(dto.identifier, this.config.login.identifierType)) {
3223
+ // Non-enumerating: return success without sending
2372
3224
  return response;
2373
3225
  }
2374
3226
  const user = await this.findUserByIdentifier(dto.identifier, this.config.login?.identifierType);
2375
3227
  if (!user) {
2376
- return response;
3228
+ return response; // Non-enumerating
2377
3229
  }
3230
+ // Only password-capable accounts can use forgot-password.
3231
+ // Hybrid (password + social) is allowed (passwordHash exists).
2378
3232
  if (!user.passwordHash) {
3233
+ // ============================================================================
3234
+ // Security: record attempt for social-only accounts (no password set)
3235
+ // ============================================================================
3236
+ // WHY: A malicious actor may spam forgot-password to learn about accounts or to harass users.
3237
+ // We keep the API response non-enumerating, but still record an audit event for observability.
2379
3238
  this.logger?.warn?.(`Password reset requested for social-only account; ignoring for user: ${user.sub}`);
2380
3239
  try {
2381
3240
  await this.auditService?.recordEvent({
@@ -2397,8 +3256,12 @@ class AuthService {
2397
3256
  return response;
2398
3257
  }
2399
3258
  const verificationMethod = this.config.signup?.verificationMethod ?? 'email';
3259
+ // ============================================================================
3260
+ // Determine delivery channel
3261
+ // ============================================================================
2400
3262
  let delivery;
2401
3263
  if (verificationMethod === 'none') {
3264
+ // Rare config: no verification required. Still prefer email if present, else phone.
2402
3265
  if (user.email)
2403
3266
  delivery = 'email';
2404
3267
  else if (user.phone)
@@ -2419,6 +3282,7 @@ class AuthService {
2419
3282
  delivery = 'sms';
2420
3283
  }
2421
3284
  if (!delivery) {
3285
+ // Non-enumerating: return success without sending
2422
3286
  return response;
2423
3287
  }
2424
3288
  try {
@@ -2428,20 +3292,36 @@ class AuthService {
2428
3292
  response.expiresIn = result.expiresIn;
2429
3293
  }
2430
3294
  catch (error) {
3295
+ // Rate limit is safe to return (still does not reveal existence when user exists).
2431
3296
  if (error instanceof nauth_exception_1.NAuthException && error.code === error_codes_enum_1.AuthErrorCode.RATE_LIMIT_PASSWORD_RESET) {
2432
3297
  throw error;
2433
3298
  }
3299
+ // Non-blocking: log and return success
2434
3300
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2435
3301
  this.logger?.error?.(`Failed to send password reset code: ${errorMessage}`, { error });
2436
3302
  }
2437
3303
  return response;
2438
3304
  }
3305
+ /**
3306
+ * Confirm a password reset by validating the reset code and setting a new password.
3307
+ *
3308
+ * Security:
3309
+ * - Uses platform-agnostic errors via NAuthException
3310
+ * - Verifies reset code via PasswordResetService
3311
+ * - Enforces password policy and history
3312
+ * - Revokes all sessions upon successful reset
3313
+ *
3314
+ * @param dto - Confirm forgot password payload
3315
+ * @returns Success response
3316
+ * @throws {NAuthException} PASSWORD_RESET_CODE_INVALID | PASSWORD_RESET_CODE_EXPIRED | PASSWORD_RESET_MAX_ATTEMPTS
3317
+ */
2439
3318
  async confirmForgotPassword(dto) {
2440
3319
  if (!this.passwordResetService) {
2441
3320
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SERVICE_UNAVAILABLE, 'Password reset is not available');
2442
3321
  }
2443
3322
  const user = await this.findUserByIdentifier(dto.identifier, this.config.login?.identifierType);
2444
3323
  if (!user || !user.passwordHash) {
3324
+ // Non-enumerating: treat as invalid code
2445
3325
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_RESET_CODE_INVALID, 'Invalid password reset code');
2446
3326
  }
2447
3327
  const { sessionsRevoked: _sessionsRevoked } = await this.updateUserPassword({
@@ -2451,6 +3331,7 @@ class AuthService {
2451
3331
  revokeSessions: true,
2452
3332
  revokeReason: 'Password reset',
2453
3333
  beforePersist: async () => {
3334
+ // Consume code (throws if invalid/expired/too many attempts)
2454
3335
  await this.passwordResetService.consumeValidCode(user, dto.code);
2455
3336
  },
2456
3337
  audit: {
@@ -2463,12 +3344,37 @@ class AuthService {
2463
3344
  });
2464
3345
  return { success: true, mustChangePassword: false };
2465
3346
  }
3347
+ // ============================================================================
3348
+ // Internal Password Update Orchestration (Single Source of Truth)
3349
+ // ============================================================================
3350
+ /**
3351
+ * Centralized password update flow used by:
3352
+ * - changePassword()
3353
+ * - confirmForgotPassword()
3354
+ * - adminSetPassword()
3355
+ * - FORCE_CHANGE_PASSWORD challenge handler
3356
+ *
3357
+ * WHY:
3358
+ * - Prevent logic drift between different password-changing entrypoints
3359
+ * - Ensure consistent validation, history enforcement, persistence, session revocation, and audit trails
3360
+ *
3361
+ * @param params - Password update parameters
3362
+ * @returns Sessions revoked count (0 when not revoked)
3363
+ * @throws {NAuthException} WEAK_PASSWORD | PASSWORD_REUSED | NOT_FOUND
3364
+ */
2466
3365
  async updateUserPassword(params) {
2467
3366
  const { user, newPassword, mustChangePassword, revokeSessions, revokeReason, beforePersist, audit } = params;
3367
+ // ============================================================================
3368
+ // Load full user entity (important for passwordHistory serialization + reuse checks)
3369
+ // ============================================================================
3370
+ // WHY: Some call sites use a slim projection (e.g., findUserByIdentifier) which may omit passwordHistory.
2468
3371
  const userEntity = (await this.userRepository.findOne({ where: { id: user.id } }));
2469
3372
  if (!userEntity) {
2470
3373
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
2471
3374
  }
3375
+ // ============================================================================
3376
+ // Validate new password + history
3377
+ // ============================================================================
2472
3378
  const validation = await this.passwordService.validatePassword(newPassword, {
2473
3379
  email: userEntity.email,
2474
3380
  username: userEntity.username || undefined,
@@ -2488,9 +3394,13 @@ class AuthService {
2488
3394
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_REUSED, 'Cannot reuse a recent password');
2489
3395
  }
2490
3396
  }
3397
+ // Hook point for flows that must prove possession of a reset code before persisting (forgot-password confirm)
2491
3398
  if (beforePersist) {
2492
3399
  await beforePersist();
2493
3400
  }
3401
+ // ============================================================================
3402
+ // Persist password update
3403
+ // ============================================================================
2494
3404
  const newHash = await this.passwordService.hashPassword(newPassword);
2495
3405
  const newHistory = userEntity.passwordHash
2496
3406
  ? this.passwordService.addToHistory(userEntity.passwordHistory || [], userEntity.passwordHash)
@@ -2500,10 +3410,16 @@ class AuthService {
2500
3410
  userEntity.passwordHistory = newHistory;
2501
3411
  userEntity.mustChangePassword = mustChangePassword;
2502
3412
  await this.userRepository.save(userEntity);
3413
+ // ============================================================================
3414
+ // Session revocation
3415
+ // ============================================================================
2503
3416
  let sessionsRevoked = 0;
2504
3417
  if (revokeSessions) {
2505
3418
  sessionsRevoked = await this.sessionService.revokeAllUserSessions(userEntity.id, revokeReason);
2506
3419
  }
3420
+ // ============================================================================
3421
+ // Audit
3422
+ // ============================================================================
2507
3423
  if (audit) {
2508
3424
  try {
2509
3425
  await this.auditService?.recordEvent({