@techelusive/nest-auth 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/LICENSE +21 -0
  3. package/README.md +204 -2
  4. package/api/openapi.json +4399 -0
  5. package/api/techelusive-authentication.postman_collection.json +3178 -0
  6. package/dist/auth-options.d.ts +156 -0
  7. package/dist/auth-options.js +3 -0
  8. package/dist/auth-options.js.map +1 -0
  9. package/dist/auth-options.validation.d.ts +9 -0
  10. package/dist/auth-options.validation.js +74 -0
  11. package/dist/auth-options.validation.js.map +1 -0
  12. package/dist/auth.module.d.ts +38 -0
  13. package/dist/auth.module.js +173 -0
  14. package/dist/auth.module.js.map +1 -0
  15. package/dist/common/auth-error.d.ts +16 -0
  16. package/dist/common/auth-error.js +23 -0
  17. package/dist/common/auth-error.js.map +1 -0
  18. package/dist/common/auth-exception.filter.d.ts +24 -0
  19. package/dist/common/auth-exception.filter.js +66 -0
  20. package/dist/common/auth-exception.filter.js.map +1 -0
  21. package/dist/common/crypto/jwt.d.ts +89 -0
  22. package/dist/common/crypto/jwt.js +144 -0
  23. package/dist/common/crypto/jwt.js.map +1 -0
  24. package/dist/common/crypto/random-secrets.d.ts +78 -0
  25. package/dist/common/crypto/random-secrets.js +113 -0
  26. package/dist/common/crypto/random-secrets.js.map +1 -0
  27. package/dist/common/crypto/remote-public-keys.d.ts +49 -0
  28. package/dist/common/crypto/remote-public-keys.js +105 -0
  29. package/dist/common/crypto/remote-public-keys.js.map +1 -0
  30. package/dist/common/crypto/totp.d.ts +68 -0
  31. package/dist/common/crypto/totp.js +148 -0
  32. package/dist/common/crypto/totp.js.map +1 -0
  33. package/dist/common/encryption.service.d.ts +36 -0
  34. package/dist/common/encryption.service.js +111 -0
  35. package/dist/common/encryption.service.js.map +1 -0
  36. package/dist/common/identifiers.d.ts +48 -0
  37. package/dist/common/identifiers.js +89 -0
  38. package/dist/common/identifiers.js.map +1 -0
  39. package/dist/common/network-safety.d.ts +19 -0
  40. package/dist/common/network-safety.js +72 -0
  41. package/dist/common/network-safety.js.map +1 -0
  42. package/dist/common/rate-limiter.service.d.ts +63 -0
  43. package/dist/common/rate-limiter.service.js +158 -0
  44. package/dist/common/rate-limiter.service.js.map +1 -0
  45. package/dist/common/refresh-token-cookie.d.ts +78 -0
  46. package/dist/common/refresh-token-cookie.js +185 -0
  47. package/dist/common/refresh-token-cookie.js.map +1 -0
  48. package/dist/common/request-context.d.ts +14 -0
  49. package/dist/common/request-context.js +22 -0
  50. package/dist/common/request-context.js.map +1 -0
  51. package/dist/common/strict-validation.pipe.d.ts +8 -0
  52. package/dist/common/strict-validation.pipe.js +16 -0
  53. package/dist/common/strict-validation.pipe.js.map +1 -0
  54. package/dist/common/time.d.ts +15 -0
  55. package/dist/common/time.js +34 -0
  56. package/dist/common/time.js.map +1 -0
  57. package/dist/constants.d.ts +1326 -0
  58. package/dist/constants.js +1142 -0
  59. package/dist/constants.js.map +1 -0
  60. package/dist/database/create-auth-tables.migration.d.ts +26 -0
  61. package/dist/database/create-auth-tables.migration.js +291 -0
  62. package/dist/database/create-auth-tables.migration.js.map +1 -0
  63. package/dist/database/email-change-and-retention.migration.d.ts +25 -0
  64. package/dist/database/email-change-and-retention.migration.js +54 -0
  65. package/dist/database/email-change-and-retention.migration.js.map +1 -0
  66. package/dist/database/entities/api-key.entity.d.ts +17 -0
  67. package/dist/database/entities/api-key.entity.js +80 -0
  68. package/dist/database/entities/api-key.entity.js.map +1 -0
  69. package/dist/database/entities/audit-log.entity.d.ts +13 -0
  70. package/dist/database/entities/audit-log.entity.js +64 -0
  71. package/dist/database/entities/audit-log.entity.js.map +1 -0
  72. package/dist/database/entities/backup-code.entity.d.ts +9 -0
  73. package/dist/database/entities/backup-code.entity.js +52 -0
  74. package/dist/database/entities/backup-code.entity.js.map +1 -0
  75. package/dist/database/entities/index.d.ts +43 -0
  76. package/dist/database/entities/index.js +82 -0
  77. package/dist/database/entities/index.js.map +1 -0
  78. package/dist/database/entities/json-object.d.ts +12 -0
  79. package/dist/database/entities/json-object.js +3 -0
  80. package/dist/database/entities/json-object.js.map +1 -0
  81. package/dist/database/entities/link-token.entity.d.ts +15 -0
  82. package/dist/database/entities/link-token.entity.js +64 -0
  83. package/dist/database/entities/link-token.entity.js.map +1 -0
  84. package/dist/database/entities/one-time-code.entity.d.ts +16 -0
  85. package/dist/database/entities/one-time-code.entity.js +69 -0
  86. package/dist/database/entities/one-time-code.entity.js.map +1 -0
  87. package/dist/database/entities/organization-invitation.entity.d.ts +14 -0
  88. package/dist/database/entities/organization-invitation.entity.js +73 -0
  89. package/dist/database/entities/organization-invitation.entity.js.map +1 -0
  90. package/dist/database/entities/organization-member.entity.d.ts +9 -0
  91. package/dist/database/entities/organization-member.entity.js +48 -0
  92. package/dist/database/entities/organization-member.entity.js.map +1 -0
  93. package/dist/database/entities/organization.entity.d.ts +12 -0
  94. package/dist/database/entities/organization.entity.js +58 -0
  95. package/dist/database/entities/organization.entity.js.map +1 -0
  96. package/dist/database/entities/passkey.entity.d.ts +19 -0
  97. package/dist/database/entities/passkey.entity.js +78 -0
  98. package/dist/database/entities/passkey.entity.js.map +1 -0
  99. package/dist/database/entities/password.entity.d.ts +6 -0
  100. package/dist/database/entities/password.entity.js +36 -0
  101. package/dist/database/entities/password.entity.js.map +1 -0
  102. package/dist/database/entities/refresh-token.entity.d.ts +9 -0
  103. package/dist/database/entities/refresh-token.entity.js +53 -0
  104. package/dist/database/entities/refresh-token.entity.js.map +1 -0
  105. package/dist/database/entities/session.entity.d.ts +20 -0
  106. package/dist/database/entities/session.entity.js +87 -0
  107. package/dist/database/entities/session.entity.js.map +1 -0
  108. package/dist/database/entities/settings.entity.d.ts +7 -0
  109. package/dist/database/entities/settings.entity.js +41 -0
  110. package/dist/database/entities/settings.entity.js.map +1 -0
  111. package/dist/database/entities/sign-in-challenge.entity.d.ts +23 -0
  112. package/dist/database/entities/sign-in-challenge.entity.js +80 -0
  113. package/dist/database/entities/sign-in-challenge.entity.js.map +1 -0
  114. package/dist/database/entities/signing-key.entity.d.ts +11 -0
  115. package/dist/database/entities/signing-key.entity.js +54 -0
  116. package/dist/database/entities/signing-key.entity.js.map +1 -0
  117. package/dist/database/entities/social-account.entity.d.ts +13 -0
  118. package/dist/database/entities/social-account.entity.js +54 -0
  119. package/dist/database/entities/social-account.entity.js.map +1 -0
  120. package/dist/database/entities/two-factor-secret.entity.d.ts +10 -0
  121. package/dist/database/entities/two-factor-secret.entity.js +48 -0
  122. package/dist/database/entities/two-factor-secret.entity.js.map +1 -0
  123. package/dist/database/entities/user.entity.d.ts +34 -0
  124. package/dist/database/entities/user.entity.js +129 -0
  125. package/dist/database/entities/user.entity.js.map +1 -0
  126. package/dist/database/entities/webhook-delivery.entity.d.ts +17 -0
  127. package/dist/database/entities/webhook-delivery.entity.js +79 -0
  128. package/dist/database/entities/webhook-delivery.entity.js.map +1 -0
  129. package/dist/database/entities/webhook.entity.d.ts +12 -0
  130. package/dist/database/entities/webhook.entity.js +58 -0
  131. package/dist/database/entities/webhook.entity.js.map +1 -0
  132. package/dist/database/typeorm-row.d.ts +21 -0
  133. package/dist/database/typeorm-row.js +29 -0
  134. package/dist/database/typeorm-row.js.map +1 -0
  135. package/dist/features/account/account.controller.d.ts +259 -0
  136. package/dist/features/account/account.controller.js +372 -0
  137. package/dist/features/account/account.controller.js.map +1 -0
  138. package/dist/features/account/account.dto.d.ts +16 -0
  139. package/dist/features/account/account.dto.js +61 -0
  140. package/dist/features/account/account.dto.js.map +1 -0
  141. package/dist/features/account/data-export.service.d.ts +85 -0
  142. package/dist/features/account/data-export.service.js +125 -0
  143. package/dist/features/account/data-export.service.js.map +1 -0
  144. package/dist/features/account/health.service.d.ts +30 -0
  145. package/dist/features/account/health.service.js +59 -0
  146. package/dist/features/account/health.service.js.map +1 -0
  147. package/dist/features/admin/admin.controller.d.ts +382 -0
  148. package/dist/features/admin/admin.controller.js +623 -0
  149. package/dist/features/admin/admin.controller.js.map +1 -0
  150. package/dist/features/admin/admin.dto.d.ts +61 -0
  151. package/dist/features/admin/admin.dto.js +253 -0
  152. package/dist/features/admin/admin.dto.js.map +1 -0
  153. package/dist/features/admin/admin.service.d.ts +142 -0
  154. package/dist/features/admin/admin.service.js +227 -0
  155. package/dist/features/admin/admin.service.js.map +1 -0
  156. package/dist/features/api-keys/api-keys.service.d.ts +67 -0
  157. package/dist/features/api-keys/api-keys.service.js +144 -0
  158. package/dist/features/api-keys/api-keys.service.js.map +1 -0
  159. package/dist/features/email-change/email-change.controller.d.ts +38 -0
  160. package/dist/features/email-change/email-change.controller.js +85 -0
  161. package/dist/features/email-change/email-change.controller.js.map +1 -0
  162. package/dist/features/email-change/email-change.dto.d.ts +10 -0
  163. package/dist/features/email-change/email-change.dto.js +43 -0
  164. package/dist/features/email-change/email-change.dto.js.map +1 -0
  165. package/dist/features/email-change/email-change.service.d.ts +74 -0
  166. package/dist/features/email-change/email-change.service.js +179 -0
  167. package/dist/features/email-change/email-change.service.js.map +1 -0
  168. package/dist/features/email-links/email-verification.service.d.ts +46 -0
  169. package/dist/features/email-links/email-verification.service.js +94 -0
  170. package/dist/features/email-links/email-verification.service.js.map +1 -0
  171. package/dist/features/email-links/link-tokens.service.d.ts +44 -0
  172. package/dist/features/email-links/link-tokens.service.js +122 -0
  173. package/dist/features/email-links/link-tokens.service.js.map +1 -0
  174. package/dist/features/events/auth-events.service.d.ts +52 -0
  175. package/dist/features/events/auth-events.service.js +117 -0
  176. package/dist/features/events/auth-events.service.js.map +1 -0
  177. package/dist/features/maintenance/data-retention.service.d.ts +60 -0
  178. package/dist/features/maintenance/data-retention.service.js +182 -0
  179. package/dist/features/maintenance/data-retention.service.js.map +1 -0
  180. package/dist/features/notifications/notifications.service.d.ts +40 -0
  181. package/dist/features/notifications/notifications.service.js +94 -0
  182. package/dist/features/notifications/notifications.service.js.map +1 -0
  183. package/dist/features/one-time-codes/one-time-codes.service.d.ts +40 -0
  184. package/dist/features/one-time-codes/one-time-codes.service.js +102 -0
  185. package/dist/features/one-time-codes/one-time-codes.service.js.map +1 -0
  186. package/dist/features/organizations/organizations.controller.d.ts +220 -0
  187. package/dist/features/organizations/organizations.controller.js +337 -0
  188. package/dist/features/organizations/organizations.controller.js.map +1 -0
  189. package/dist/features/organizations/organizations.dto.d.ts +31 -0
  190. package/dist/features/organizations/organizations.dto.js +100 -0
  191. package/dist/features/organizations/organizations.dto.js.map +1 -0
  192. package/dist/features/organizations/organizations.service.d.ts +274 -0
  193. package/dist/features/organizations/organizations.service.js +481 -0
  194. package/dist/features/organizations/organizations.service.js.map +1 -0
  195. package/dist/features/passkeys/passkeys.controller.d.ts +92 -0
  196. package/dist/features/passkeys/passkeys.controller.js +183 -0
  197. package/dist/features/passkeys/passkeys.controller.js.map +1 -0
  198. package/dist/features/passkeys/passkeys.dto.d.ts +13 -0
  199. package/dist/features/passkeys/passkeys.dto.js +50 -0
  200. package/dist/features/passkeys/passkeys.dto.js.map +1 -0
  201. package/dist/features/passkeys/passkeys.service.d.ts +159 -0
  202. package/dist/features/passkeys/passkeys.service.js +350 -0
  203. package/dist/features/passkeys/passkeys.service.js.map +1 -0
  204. package/dist/features/passwordless/passwordless-auth.service.d.ts +152 -0
  205. package/dist/features/passwordless/passwordless-auth.service.js +304 -0
  206. package/dist/features/passwordless/passwordless-auth.service.js.map +1 -0
  207. package/dist/features/passwordless/passwordless.controller.d.ts +75 -0
  208. package/dist/features/passwordless/passwordless.controller.js +163 -0
  209. package/dist/features/passwordless/passwordless.controller.js.map +1 -0
  210. package/dist/features/passwordless/passwordless.dto.d.ts +29 -0
  211. package/dist/features/passwordless/passwordless.dto.js +101 -0
  212. package/dist/features/passwordless/passwordless.dto.js.map +1 -0
  213. package/dist/features/passwords/password-auth.service.d.ts +154 -0
  214. package/dist/features/passwords/password-auth.service.js +317 -0
  215. package/dist/features/passwords/password-auth.service.js.map +1 -0
  216. package/dist/features/passwords/password-policy.service.d.ts +63 -0
  217. package/dist/features/passwords/password-policy.service.js +175 -0
  218. package/dist/features/passwords/password-policy.service.js.map +1 -0
  219. package/dist/features/passwords/password.controller.d.ts +95 -0
  220. package/dist/features/passwords/password.controller.js +193 -0
  221. package/dist/features/passwords/password.controller.js.map +1 -0
  222. package/dist/features/passwords/password.dto.d.ts +36 -0
  223. package/dist/features/passwords/password.dto.js +140 -0
  224. package/dist/features/passwords/password.dto.js.map +1 -0
  225. package/dist/features/sessions/session.service.d.ts +132 -0
  226. package/dist/features/sessions/session.service.js +308 -0
  227. package/dist/features/sessions/session.service.js.map +1 -0
  228. package/dist/features/sessions/session.types.d.ts +34 -0
  229. package/dist/features/sessions/session.types.js +3 -0
  230. package/dist/features/sessions/session.types.js.map +1 -0
  231. package/dist/features/settings/settings.defaults.d.ts +2 -0
  232. package/dist/features/settings/settings.defaults.js +7 -0
  233. package/dist/features/settings/settings.defaults.js.map +1 -0
  234. package/dist/features/settings/settings.service.d.ts +44 -0
  235. package/dist/features/settings/settings.service.js +112 -0
  236. package/dist/features/settings/settings.service.js.map +1 -0
  237. package/dist/features/settings/settings.types.d.ts +133 -0
  238. package/dist/features/settings/settings.types.js +3 -0
  239. package/dist/features/settings/settings.types.js.map +1 -0
  240. package/dist/features/settings/settings.validation.d.ts +33 -0
  241. package/dist/features/settings/settings.validation.js +614 -0
  242. package/dist/features/settings/settings.validation.js.map +1 -0
  243. package/dist/features/sign-in/sign-in-challenges.service.d.ts +54 -0
  244. package/dist/features/sign-in/sign-in-challenges.service.js +104 -0
  245. package/dist/features/sign-in/sign-in-challenges.service.js.map +1 -0
  246. package/dist/features/sign-in/sign-in.service.d.ts +127 -0
  247. package/dist/features/sign-in/sign-in.service.js +248 -0
  248. package/dist/features/sign-in/sign-in.service.js.map +1 -0
  249. package/dist/features/sign-in/sign-in.types.d.ts +22 -0
  250. package/dist/features/sign-in/sign-in.types.js +3 -0
  251. package/dist/features/sign-in/sign-in.types.js.map +1 -0
  252. package/dist/features/signing-keys/jwks.controller.d.ts +17 -0
  253. package/dist/features/signing-keys/jwks.controller.js +51 -0
  254. package/dist/features/signing-keys/jwks.controller.js.map +1 -0
  255. package/dist/features/signing-keys/signing-keys.service.d.ts +67 -0
  256. package/dist/features/signing-keys/signing-keys.service.js +142 -0
  257. package/dist/features/signing-keys/signing-keys.service.js.map +1 -0
  258. package/dist/features/social/social-auth.service.d.ts +100 -0
  259. package/dist/features/social/social-auth.service.js +232 -0
  260. package/dist/features/social/social-auth.service.js.map +1 -0
  261. package/dist/features/social/social-providers.service.d.ts +97 -0
  262. package/dist/features/social/social-providers.service.js +214 -0
  263. package/dist/features/social/social-providers.service.js.map +1 -0
  264. package/dist/features/social/social.controller.d.ts +54 -0
  265. package/dist/features/social/social.controller.js +124 -0
  266. package/dist/features/social/social.controller.js.map +1 -0
  267. package/dist/features/social/social.dto.d.ts +11 -0
  268. package/dist/features/social/social.dto.js +38 -0
  269. package/dist/features/social/social.dto.js.map +1 -0
  270. package/dist/features/two-factor/two-factor.controller.d.ts +84 -0
  271. package/dist/features/two-factor/two-factor.controller.js +174 -0
  272. package/dist/features/two-factor/two-factor.controller.js.map +1 -0
  273. package/dist/features/two-factor/two-factor.dto.d.ts +10 -0
  274. package/dist/features/two-factor/two-factor.dto.js +37 -0
  275. package/dist/features/two-factor/two-factor.dto.js.map +1 -0
  276. package/dist/features/two-factor/two-factor.service.d.ts +171 -0
  277. package/dist/features/two-factor/two-factor.service.js +348 -0
  278. package/dist/features/two-factor/two-factor.service.js.map +1 -0
  279. package/dist/features/users/sign-in-methods.service.d.ts +40 -0
  280. package/dist/features/users/sign-in-methods.service.js +81 -0
  281. package/dist/features/users/sign-in-methods.service.js.map +1 -0
  282. package/dist/features/users/sign-up-rules.d.ts +12 -0
  283. package/dist/features/users/sign-up-rules.js +29 -0
  284. package/dist/features/users/sign-up-rules.js.map +1 -0
  285. package/dist/features/users/users.service.d.ts +246 -0
  286. package/dist/features/users/users.service.js +509 -0
  287. package/dist/features/users/users.service.js.map +1 -0
  288. package/dist/features/users/users.types.d.ts +24 -0
  289. package/dist/features/users/users.types.js +3 -0
  290. package/dist/features/users/users.types.js.map +1 -0
  291. package/dist/features/webhooks/webhook-delivery.worker.d.ts +55 -0
  292. package/dist/features/webhooks/webhook-delivery.worker.js +175 -0
  293. package/dist/features/webhooks/webhook-delivery.worker.js.map +1 -0
  294. package/dist/features/webhooks/webhook-signature.d.ts +28 -0
  295. package/dist/features/webhooks/webhook-signature.js +85 -0
  296. package/dist/features/webhooks/webhook-signature.js.map +1 -0
  297. package/dist/features/webhooks/webhooks.service.d.ts +144 -0
  298. package/dist/features/webhooks/webhooks.service.js +238 -0
  299. package/dist/features/webhooks/webhooks.service.js.map +1 -0
  300. package/dist/guards/auth.decorators.d.ts +65 -0
  301. package/dist/guards/auth.decorators.js +68 -0
  302. package/dist/guards/auth.decorators.js.map +1 -0
  303. package/dist/guards/auth.guard.d.ts +47 -0
  304. package/dist/guards/auth.guard.js +132 -0
  305. package/dist/guards/auth.guard.js.map +1 -0
  306. package/dist/index.d.ts +38 -0
  307. package/dist/index.js +95 -0
  308. package/dist/index.js.map +1 -0
  309. package/dist/integrations/console-email-sender.d.ts +15 -0
  310. package/dist/integrations/console-email-sender.js +26 -0
  311. package/dist/integrations/console-email-sender.js.map +1 -0
  312. package/dist/integrations/msg91-sms-sender.d.ts +28 -0
  313. package/dist/integrations/msg91-sms-sender.js +46 -0
  314. package/dist/integrations/msg91-sms-sender.js.map +1 -0
  315. package/docs/README.md +39 -0
  316. package/docs/admin-api-keys-webhooks.md +147 -0
  317. package/docs/api-reference.md +175 -0
  318. package/docs/architecture.md +119 -0
  319. package/docs/client-integration.md +192 -0
  320. package/docs/code-walkthrough.md +273 -0
  321. package/docs/configuration.md +229 -0
  322. package/docs/endpoints/README.md +151 -0
  323. package/docs/endpoints/account.md +363 -0
  324. package/docs/endpoints/admin.md +422 -0
  325. package/docs/endpoints/email-password.md +261 -0
  326. package/docs/endpoints/organizations.md +192 -0
  327. package/docs/endpoints/passkeys.md +195 -0
  328. package/docs/endpoints/passwordless.md +214 -0
  329. package/docs/endpoints/social.md +116 -0
  330. package/docs/endpoints/two-factor.md +146 -0
  331. package/docs/error-codes.md +100 -0
  332. package/docs/events.md +83 -0
  333. package/docs/getting-started.md +139 -0
  334. package/docs/manual-testing.md +215 -0
  335. package/docs/organizations.md +86 -0
  336. package/docs/production-checklist.md +60 -0
  337. package/docs/protecting-routes.md +147 -0
  338. package/docs/security.md +61 -0
  339. package/docs/sign-in-flows.md +294 -0
  340. package/package.json +92 -4
@@ -0,0 +1,119 @@
1
+ # Architecture
2
+
3
+ For contributors, and for anyone who wants to know what happens inside.
4
+
5
+ ## Layout
6
+
7
+ ```
8
+ src/
9
+ ├── index.ts public exports (the only import path apps use)
10
+ ├── auth.module.ts AuthModule.forRoot(): wires providers, controllers, guard, filter
11
+ ├── auth-options.ts AuthOptions and sender interfaces
12
+ ├── auth-options.validation.ts startup checks of the options
13
+ ├── constants.ts ALL constants: limits, lifetimes, patterns, errors, events
14
+ ├── common/ building blocks with no business rules
15
+ │ ├── crypto/ jwt (ES256/RS256), totp (RFC 6238), random-secrets, remote-public-keys
16
+ │ ├── auth-error.ts AuthError(AUTH_ERRORS.x) → { status, code, message }
17
+ │ ├── auth-exception.filter one error format for the whole app
18
+ │ ├── encryption.service KMS / local AES-GCM envelope with context binding
19
+ │ ├── rate-limiter.service Redis or memory, fails closed
20
+ │ ├── network-safety.ts SSRF guard for outgoing webhook calls
21
+ │ └── identifiers.ts email / phone normalization
22
+ ├── database/
23
+ │ ├── entities/ one TypeORM entity per table
24
+ │ └── create-auth-tables.migration.ts
25
+ ├── guards/ AuthGuard + decorators
26
+ ├── integrations/ ready-made senders (MSG91, console email)
27
+ └── features/ one folder per feature: service (+ controller + dto)
28
+ ├── settings/ defaults → code → admin layering, validation
29
+ ├── signing-keys/ ES256 key creation, rotation, JWKS endpoint
30
+ ├── sessions/ sessions, access and refresh tokens
31
+ ├── users/ account life cycle, sign-up rules, sign-in method inventory
32
+ ├── sign-in/ completeSignIn pipeline, 2FA / reactivation challenges
33
+ ├── passwords/ register, login, reset, change, password policy
34
+ ├── email-links/ link tokens, email verification
35
+ ├── one-time-codes/ email/SMS codes
36
+ ├── passwordless/ email OTP, magic link, phone OTP
37
+ ├── social/ Google / Apple / Facebook verification and linking
38
+ ├── passkeys/ WebAuthn via @simplewebauthn/server
39
+ ├── two-factor/ TOTP + backup codes
40
+ ├── organizations/ organizations, members, invitations
41
+ ├── api-keys/ machine keys
42
+ ├── webhooks/ admin CRUD, signatures, delivery worker
43
+ ├── events/ audit log → onEvent → webhook queue
44
+ ├── notifications/ wraps the app's email/SMS senders
45
+ ├── account/ /auth/me, sessions, refresh, deactivate
46
+ └── admin/ admin API
47
+ ```
48
+
49
+ ## The sign-in pipeline
50
+
51
+ Every method only **proves identity**, then hands over to `SignInService.completeSignIn()`:
52
+
53
+ ```
54
+ password │ email code │ magic link │ SMS code │ Google/Apple/Facebook │ passkey
55
+ └─────────────────────────────┬──────────────────────────────────────┘
56
+ ▼
57
+ SignInService.completeSignIn(user, method)
58
+ 1. account status check (NOT_VERIFIED / DEACTIVATED / SUSPENDED)
59
+ 2. 2FA on? → challenge token (except passkeys)
60
+ 3. SessionService.startSession → tokens
61
+ 4. lastSignedInAt, user.signed_in event, new-device email
62
+ ```
63
+
64
+ Adding a sign-in method therefore can't skip a security check: it only has to produce a `UserEntity`.
65
+
66
+ ## Data model
67
+
68
+ ```
69
+ auth_users ─┬─ auth_passwords (1:1)
70
+ ├─ auth_social_accounts (1:n, unique provider+provider_user_id)
71
+ ├─ auth_passkeys (1:n)
72
+ ├─ auth_two_factor_secrets (1:1) ─ auth_backup_codes (1:n)
73
+ ├─ auth_sessions (1:n) ─ auth_refresh_tokens (1:n)
74
+ ├─ auth_organization_members (n:m) ─ auth_organizations ─ auth_organization_invitations
75
+ └─ auth_audit_logs
76
+ auth_link_tokens, auth_one_time_codes, auth_sign_in_challenges short-lived, single-use
77
+ auth_signing_keys one current key (partial unique index), retired keys kept 2 days
78
+ auth_settings one row (id = 1): the admin's changes
79
+ auth_api_keys, auth_webhooks ─ auth_webhook_deliveries
80
+ ```
81
+
82
+ The database itself enforces key invariants:
83
+ - lowercase emails and E.164 phone numbers
84
+ - valid statuses
85
+ - every live user has an email or phone
86
+ - only one current signing key
87
+ - one settings row
88
+
89
+ ## Concurrency
90
+
91
+ Anything that must happen at most once is a single conditional `UPDATE`:
92
+
93
+ | What | How |
94
+ |---|---|
95
+ | Refresh-token use | `UPDATE ... SET used_at = now() WHERE used_at IS NULL ... RETURNING` |
96
+ | Link tokens, codes, challenges | Same pattern |
97
+ | TOTP replay | `UPDATE ... WHERE last_used_time_step < :step` |
98
+ | Status changes | `UPDATE ... WHERE status = :expected` |
99
+ | Webhook delivery claims | `FOR UPDATE SKIP LOCKED`, so many servers can run the worker |
100
+
101
+ ## Caches (per server)
102
+
103
+ | What | Lifetime | Effect |
104
+ |---|---|---|
105
+ | Settings | 10 s | Admin changes reach every server within 10 s |
106
+ | Signing keys | 60 s | Rotation reaches every server within a minute |
107
+ | Session checks (`@CheckSession`) | 5 s | Sign-out and suspension take effect within 5 s on those routes |
108
+ | Google / Apple public keys | 6 h | Refetched early when an unknown key id appears (at most once a minute) |
109
+
110
+ ## Design rules for contributors
111
+
112
+ 1. **Constants:** every route, status, name, number, pattern, header, message and code goes in `constants.ts`. `test/no-hardcoded-values.test.ts` fails the build otherwise. The only exceptions are code itself (SQL, entity property names), human-readable log text, and released migrations, which must never change.
113
+ 2. **Names:** use descriptive names, and comments that explain *why*.
114
+ 3. **Errors:** throw `new AuthError(AUTH_ERRORS.x)`. Never leak internals.
115
+ 4. **Enumeration:** never reveal whether an account exists before the user proves who they are.
116
+ 5. **Single use:** anything single-use is consumed with one atomic statement.
117
+ 6. **Events:** every security-relevant action records an event. Event details never contain secrets.
118
+ 7. **Settings:** new behavior is controlled by a setting (with validation and a secure default) or a constant. Code does not branch on environment variables.
119
+ 8. **Tests:** pure logic gets unit tests (`test/`), and SQL changes are run against a real PostgreSQL.
@@ -0,0 +1,192 @@
1
+ # Client integration
2
+
3
+ Guidance for the web and mobile apps that call the auth API.
4
+
5
+ ## Storing tokens
6
+
7
+ | Client | Access token | Refresh token |
8
+ |---|---|---|
9
+ | iOS | Memory | Keychain (`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`) |
10
+ | Android | Memory | EncryptedSharedPreferences / Android Keystore |
11
+ | React Native / Flutter | Memory | `react-native-keychain` / `flutter_secure_storage` |
12
+ | Web SPA | Memory (a JS variable) | See below |
13
+
14
+ **Never put tokens in URLs or logs.** On the web, avoid `localStorage` for the refresh token if you can: any script injected into the page (XSS) can read it. Options, from most to least secure:
15
+
16
+ 1. **Cookie mode (recommended for web):** turn on `refreshTokenCookie` and the API itself keeps the refresh token in an `HttpOnly; Secure` cookie. Page scripts never see it. See [below](#web-apps-refresh-token-in-a-cookie).
17
+ 2. **Backend-for-frontend:** a small server on your web domain calls this API and keeps the refresh token in its own cookie. Same protection; useful when you already have such a server.
18
+ 3. **In memory only:** the user signs in again after a page reload. Simple, and safe for admin dashboards.
19
+ 4. **`localStorage`:** acceptable only with a strict Content Security Policy and short sessions (`tokens.sessionDays`).
20
+
21
+ ## Calling the API
22
+
23
+ ```ts
24
+ const response = await fetch('https://api.myapp.com/rides/mine', {
25
+ headers: { Authorization: `Bearer ${accessToken}` },
26
+ });
27
+ const body = await response.json();
28
+ if (!body.success) handleError(body.code, body);
29
+ ```
30
+
31
+ Decide by `code`, never by `message` (see [Error codes](error-codes.md)).
32
+
33
+ ## Refreshing tokens
34
+
35
+ Refresh when a request fails with `401 INVALID_TOKEN`, or a little before `expiresIn` runs out. Two rules matter:
36
+
37
+ 1. **Store the new refresh token every time.** The old one is now used up.
38
+ 2. **Only one refresh at a time.** If five requests fail together and all five refresh with the same token, the second one looks like token theft and the session is ended.
39
+
40
+ ```ts
41
+ let refreshInFlight: Promise<void> | null = null;
42
+
43
+ async function refreshTokens(): Promise<void> {
44
+ // Every caller waits for the same refresh.
45
+ refreshInFlight ??= (async () => {
46
+ const response = await fetch('https://api.myapp.com/auth/refresh', {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({ refreshToken: await secureStore.get('refreshToken') }),
50
+ });
51
+ const body = await response.json();
52
+ if (!body.success) {
53
+ await signOutLocally(); // session over: go to sign-in
54
+ throw new Error(body.code);
55
+ }
56
+ accessToken = body.accessToken;
57
+ await secureStore.set('refreshToken', body.refreshToken);
58
+ })().finally(() => (refreshInFlight = null));
59
+ return refreshInFlight;
60
+ }
61
+
62
+ async function apiFetch(path: string, init: RequestInit = {}, isRetry = false): Promise<any> {
63
+ const response = await fetch(`https://api.myapp.com${path}`, {
64
+ ...init,
65
+ headers: { ...init.headers, Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
66
+ });
67
+ const body = await response.json();
68
+ if (response.status === 401 && body.code === 'INVALID_TOKEN' && !isRetry) {
69
+ await refreshTokens();
70
+ return apiFetch(path, init, true);
71
+ }
72
+ return body;
73
+ }
74
+ ```
75
+
76
+ On mobile, also make sure two app processes (e.g. the app and a widget) don't refresh at the same time.
77
+
78
+ ## Web apps: refresh token in a cookie
79
+
80
+ ```ts
81
+ AuthModule.forRoot({
82
+ useFactory: () => ({
83
+ // ...
84
+ refreshTokenCookie: { allowedOrigins: ['https://app.myapp.com'] },
85
+ }),
86
+ });
87
+ ```
88
+
89
+ Your API must also allow these origins with credentials in CORS: `app.enableCors({ origin: ['https://app.myapp.com'], credentials: true })`.
90
+
91
+ What changes, **only for requests whose `Origin` header is in `allowedOrigins`** (browsers add it themselves; mobile apps and servers don't send it, so they keep getting the refresh token in the body from the same API):
92
+
93
+ - Every response that used to contain `refreshToken` now sets the cookie `__Host-refresh_token` (`HttpOnly; Secure; SameSite=Strict; Path=/`) and says `"refreshTokenInCookie": true` instead.
94
+ - Refresh with an empty body and `credentials: 'include'`. A refresh that uses the cookie is refused (`403`) unless it comes from an allowed origin, which blocks cross-site request forgery.
95
+ - Logout, logout everywhere, deactivate and delete account clear the cookie.
96
+
97
+ ```ts
98
+ async function refreshTokens(): Promise<void> {
99
+ refreshInFlight ??= (async () => {
100
+ const response = await fetch('https://api.myapp.com/auth/refresh', {
101
+ method: 'POST',
102
+ credentials: 'include', // send the cookie
103
+ headers: { 'Content-Type': 'application/json' },
104
+ body: '{}',
105
+ });
106
+ const body = await response.json();
107
+ if (!body.success) {
108
+ await signOutLocally();
109
+ throw new Error(body.code);
110
+ }
111
+ accessToken = body.accessToken; // the access token still lives in memory only
112
+ })().finally(() => (refreshInFlight = null));
113
+ return refreshInFlight;
114
+ }
115
+ ```
116
+
117
+ Send `credentials: 'include'` on sign-in requests too, so the browser stores the cookie. After a page reload, call `refreshTokens()` once to get a new access token.
118
+
119
+ Options: `name` (default `__Host-refresh_token`), `domain` (to share with subdomains; not allowed with a `__Host-` name), `path` (default `/`), `sameSite` (default `Strict`; use `Lax` when the web app and the API are on different sites, e.g. `myapp.com` and `myapp-api.com`). Browsers accept `Secure` cookies on `http://localhost`, so cookie mode works in local development.
120
+
121
+ ## Changing email
122
+
123
+ 1. Signed in: `POST /auth/email/change` with `newEmail` (and `currentPassword` if the user has a password). Show "check your new inbox".
124
+ 2. Your page for the emailed link (`confirm_email_change`) calls `POST /auth/email/change/confirm` with the token.
125
+ 3. If the response is `403 RECENT_SIGN_IN_REQUIRED`, ask the user to sign in again, then retry.
126
+
127
+ ## Handling the sign-in result
128
+
129
+ ```ts
130
+ async function handleSignInResponse(body: any) {
131
+ if (body.success && body.status === 'signed_in') return saveTokensAndEnterApp(body);
132
+ if (body.success && body.status === 'two_factor_required') return showTwoFactorScreen(body.challengeToken);
133
+
134
+ switch (body.code) {
135
+ case 'ACCOUNT_NOT_VERIFIED':
136
+ return showVerificationScreen(body.pendingVerifications, body.email, body.phoneNumber);
137
+ case 'ACCOUNT_DEACTIVATED':
138
+ return askToReactivate(body.reactivationToken); // POST /auth/reactivate
139
+ case 'ACCOUNT_SUSPENDED':
140
+ return showContactSupport();
141
+ case 'VALIDATION_ERROR':
142
+ if (body.missingSignUpDetails) return showSignUpDetailsForm(body.missingSignUpDetails); // then retry
143
+ return showFormErrors(body.problems);
144
+ case 'TOO_MANY_REQUESTS':
145
+ return showRetryLater(body.retryAfterSeconds);
146
+ default:
147
+ return showMessage(body.message);
148
+ }
149
+ }
150
+ ```
151
+
152
+ ## After sign-in
153
+
154
+ - **Onboarding:** if `user.onboardingCompleted` is `false`, show your onboarding. When it's done, your backend calls `UsersService.completeOnboarding`.
155
+ - **Pending verification:** `user.pendingVerifications` tells you what to verify, even for active users when the settings change later.
156
+ - **Profile screen:** use `GET /auth/me` and `GET /auth/sign-in-methods`.
157
+
158
+ ## Links in emails
159
+
160
+ Link tokens (`ev_`, `pr_`, `ml_`, `oi_`) arrive in your `emailSender`. Build links to your own pages or deep links:
161
+
162
+ | Email | Example link | Page calls |
163
+ |---|---|---|
164
+ | Verify email | `https://myapp.com/verify-email?token=ev_...` | `POST /auth/email/verify` |
165
+ | Reset password | `https://myapp.com/reset-password?token=pr_...` | `POST /auth/password/reset` (after the user types a new password) |
166
+ | Magic link | `https://myapp.com/magic?token=ml_...` or `myapp://magic?token=...` | `POST /auth/magic-link/verify` |
167
+ | Invitation | `https://myapp.com/join?token=oi_...` | `POST /auth/organizations/invitations/accept` (after sign-in) |
168
+
169
+ Tips:
170
+
171
+ - Call the API from the page **after a user action** (a button press), not automatically on load. Email security scanners open links automatically and would use up single-use tokens.
172
+ - Remove the token from the address bar after reading it (`history.replaceState`).
173
+ - Use universal links (iOS) or app links (Android) so magic links open the app.
174
+
175
+ ## Social sign-in on each platform
176
+
177
+ | Platform | Google | Apple | Facebook |
178
+ |---|---|---|---|
179
+ | Web | [Google Identity Services](https://developers.google.com/identity/gsi/web): use the `credential` (ID token) | [Sign in with Apple JS](https://developer.apple.com/documentation/signinwithapplejs): `authorization.id_token` | Facebook JS SDK: `authResponse.accessToken` |
180
+ | iOS | Google Sign-In SDK: `user.idToken` | `ASAuthorizationAppleIDCredential.identityToken` | Facebook iOS SDK access token |
181
+ | Android | Credential Manager + `GetGoogleIdOption`: `idToken` | Web flow | Facebook Android SDK access token |
182
+
183
+ Add every client id that issues tokens (web, iOS and Android each have one for Google) to `google.clientIds` / `apple.clientIds`.
184
+
185
+ ## Passkeys
186
+
187
+ Web: use [`@simplewebauthn/browser`](https://simplewebauthn.dev/docs/packages/browser), see [Sign-in flows](sign-in-flows.md#passkeys). Native apps use the platform APIs (iOS `ASAuthorizationPlatformPublicKeyCredentialProvider`, Android Credential Manager) and need your domain's association files:
188
+
189
+ - **iOS:** `https://myapp.com/.well-known/apple-app-site-association` with `webcredentials`
190
+ - **Android:** `https://myapp.com/.well-known/assetlinks.json` with `delegate_permission/common.get_login_creds`
191
+
192
+ Add the Android app's origin (`android:apk-key-hash:...`) to `passkeys.allowedOrigins`.
@@ -0,0 +1,273 @@
1
+ # Code walkthrough
2
+
3
+ A guided tour of the source code, for anyone who wants to understand how the library works inside, maintain it, or contribute. Each flow is traced from the HTTP request to the database, naming the file and function at every hop, so you can open them side by side.
4
+
5
+ If you only want to *use* the library, read [Getting started](getting-started.md) instead.
6
+
7
+ ## How to study it
8
+
9
+ **Watch it run, then read it.** Reading code without seeing it run is slow. The fastest way to learn a flow is to stop the server in the middle of it:
10
+
11
+ - In VS Code, open the command palette and run **JavaScript Debug Terminal**. In that terminal, run `npm run playground`. Source maps are on, so breakpoints in the `.ts` files under `src/` work.
12
+ - Put a breakpoint on the first line of the function named in a flow below (for example `signInWithPassword` in `src/features/passwords/password-auth.service.ts`).
13
+ - Send the request from Postman or Swagger. The debugger stops there; step through with **F10** (next line) and **F11** (into a function), and look at the variables panel.
14
+ - Keep the dev dashboard (http://localhost:3000/dev) open to see the emails, events and webhooks each step produces, and a database viewer (TablePlus, DBeaver or `psql`) on the `auth_*` tables to see each row appear.
15
+
16
+ **Suggested order:** the map, then "Every request", then email + password from register to refresh. Those cover most of the shared machinery; every other flow reuses it. Then read the other flows in any order.
17
+
18
+ **Background you need:** NestJS modules, controllers, providers and decorators (the official NestJS "First steps" and "Guards" pages), and basic TypeORM repositories. Security terms are explained in the code comments and in [Security](security.md).
19
+
20
+ ---
21
+
22
+ ## The map
23
+
24
+ ```
25
+ src/
26
+ index.ts everything apps can import (start here to see the public API)
27
+ auth.module.ts AuthModule.forRoot(): wires every controller and service together
28
+ auth-options.ts what an app must pass in (database, URLs, senders...)
29
+ auth-options.validation.ts checks those options at startup
30
+ constants.ts every fixed value: routes, error codes, limits, default settings
31
+ common/ small shared helpers (errors, rate limiting, encryption, crypto)
32
+ guards/ AuthGuard and the decorators (@Public, @Roles, @CurrentUser...)
33
+ database/ entities (tables) and migrations
34
+ features/ one folder per feature
35
+ passwords/ passwordless/ social/ passkeys/ two-factor/ ways to sign in
36
+ sign-in/ what happens after any method proves who the user is
37
+ sessions/ sessions, access and refresh tokens
38
+ users/ accounts and their life cycle
39
+ email-links/ one-time-codes/ notifications/ links, codes, sending them
40
+ account/ email-change/ the signed-in user's own account
41
+ organizations/ api-keys/ webhooks/ events/ admin/ settings/ signing-keys/ maintenance/
42
+ integrations/ ready-made senders (MSG91 SMS, console email)
43
+ ```
44
+
45
+ Most feature folders hold the same three kinds of file:
46
+
47
+ | File | Job |
48
+ |---|---|
49
+ | `*.controller.ts` | HTTP only: route, request body, calls one service method. No business rules. |
50
+ | `*.dto.ts` | The shape of request bodies (types and lengths). Unknown fields are rejected. |
51
+ | `*.service.ts` | The rules: what is allowed, what is saved, which events and emails go out. |
52
+
53
+ So for any endpoint: find its route in `ROUTES` (constants.ts), find the controller method with that route, and follow the one service call it makes.
54
+
55
+ ---
56
+
57
+ ## App startup
58
+
59
+ **`AuthModule.forRoot()`** in `auth.module.ts`:
60
+
61
+ - Calls the app's `useFactory` to get the `AuthOptions`, then `assertValidAuthOptions` (`auth-options.validation.ts`) stops the app with a clear list of problems if something is missing (no email sender, local key in production, no Redis in production...).
62
+ - Registers every controller and service, plus three app-wide pieces:
63
+ - `AuthGuard` as `APP_GUARD`: runs before **every** route in the app.
64
+ - `AuthExceptionFilter` as `APP_FILTER`: turns every error into `{ success: false, code, message }`.
65
+ - `RefreshTokenCookieInterceptor` as `APP_INTERCEPTOR`: only does anything in cookie mode.
66
+ - Background jobs start on `onApplicationBootstrap`: `WebhookDeliveryWorker` (every few seconds) and `DataRetentionService` (every hour).
67
+ - On the first request needing a token, `SigningKeysService.getSigningKeys` creates the first signing key if there is none.
68
+
69
+ What to notice: nothing is global or hidden. Every service gets what it needs through its constructor, and `AUTH_OPTIONS_TOKEN` is how they all reach the app's options (database, senders).
70
+
71
+ ---
72
+
73
+ ## Every request: the guard
74
+
75
+ **`AuthGuard.canActivate`** in `guards/auth.guard.ts` decides who may call any route, in this order:
76
+
77
+ - A route marked `@Public()` lets everyone in. If a token is sent anyway, it is read so `@CurrentUser()` works.
78
+ - An `x-api-key` header on a route marked `@ApiKeyScopes(...)` → `ApiKeysService.verifyApiKey` (hash lookup) and a scope check.
79
+ - Otherwise an `Authorization: Bearer` access token is required → `SessionService.verifyAccessToken` checks the signature (ES256, keys from `SigningKeysService`), issuer, audience, type and expiry. No database query: that is why access tokens are fast.
80
+ - `@CheckSession()` / `@AdminOnly()` routes also call `SessionService.isSessionStillAllowed`, which asks the database whether the session is still open and the account still active (cached for a few seconds). This is how "sign out everywhere" and "suspend" take effect before the access token expires.
81
+ - `@Roles(...)` / `@AdminOnly()` compare the roles in the token.
82
+ - The caller is attached to the request; `@CurrentUser()`, `@CurrentUserId()` and `@CurrentCaller()` (`guards/auth.decorators.ts`) read it back in controllers.
83
+
84
+ **When something fails**, services throw `new AuthError(AUTH_ERRORS.someError, extraFields?)` (`common/auth-error.ts`). `AuthExceptionFilter` (`common/auth-exception.filter.ts`) turns it into the JSON response with the right HTTP status. Unexpected errors are logged with their stack trace and answered with a generic `INTERNAL_SERVER_ERROR`.
85
+
86
+ ---
87
+
88
+ ## Email + password
89
+
90
+ ### Register: `POST /auth/register`
91
+
92
+ `PasswordController.register` → **`PasswordAuthService.register`** (`features/passwords/password-auth.service.ts`):
93
+
94
+ - `RateLimiterService.consume` counts sign-ups per IP (`common/rate-limiter.service.ts`; counters in Redis or memory).
95
+ - `normalizeEmail` (`common/identifiers.ts`) lowercases and validates the email.
96
+ - `assertRegistrationRules` applies the settings (sign-up open? allowed domains? names and terms required?).
97
+ - `PasswordPolicyService.assertPasswordIsAcceptable` checks length and character rules, and asks haveibeenpwned.com (only the first 5 characters of the SHA-1 hash are sent) whether the password was in a breach.
98
+ - **If the email already has an account**, the same "verification required" answer is returned as for a new account, so nobody can find out who is registered. If that existing account never verified, it gets a fresh verification email.
99
+ - Otherwise, in one database transaction: `UsersService.createUser` (default roles, status `PENDING_VERIFICATION` or `ACTIVE`, event `user.created`) and the Argon2id hash (`hashPassword`) goes into `auth_passwords`.
100
+ - If verification is needed, `EmailVerificationService.sendVerificationEmail` → `LinkTokensService.createLinkToken` (random token; only its SHA-256 hash is stored) → `NotificationsService.sendRequiredEmail` → the app's `emailSender`.
101
+
102
+ ### Verify email: `POST /auth/email/verify`
103
+
104
+ `EmailVerificationService.verifyEmail` (`features/email-links/email-verification.service.ts`):
105
+
106
+ - `LinkTokensService.useLinkToken` marks the token used **in one UPDATE ... WHERE used_at IS NULL**. If two clicks arrive together, only one gets a row back, so a link can never be used twice.
107
+ - `UsersService.markEmailVerified` → `activateIfReady` moves the account to `ACTIVE` once nothing is left to verify (events `user.email_verified`, `user.activated`).
108
+
109
+ ### Login: `POST /auth/login`
110
+
111
+ `PasswordController.login` → **`PasswordAuthService.signInWithPassword`**:
112
+
113
+ - Rate limits per IP **and** per account (stops password guessing on one account from many IPs).
114
+ - `PasswordPolicyService.verifyPassword` always runs the slow Argon2 check, even when the email has no account (against a dummy hash), so the response time doesn't reveal which emails exist.
115
+ - Wrong email or password → `SignInService.recordFailedSignIn` (event `user.sign_in_failed`) and the same `INVALID_CREDENTIALS` either way.
116
+ - Right password → the account's counter is reset, an old-style hash is upgraded (`needsRehash`), then **`SignInService.completeSignIn`**.
117
+
118
+ ### The shared ending: `completeSignIn`
119
+
120
+ Every sign-in method (password, codes, links, Google, passkeys) ends in **`SignInService.completeSignIn`** (`features/sign-in/sign-in.service.ts`). This is the most important function to understand: because every method goes through it, no method can skip a check.
121
+
122
+ - `assertAccountCanSignIn` looks at the account status:
123
+ - `PENDING_VERIFICATION` → `ACCOUNT_NOT_VERIFIED` with `pendingVerifications`
124
+ - `DEACTIVATED` → `ACCOUNT_DEACTIVATED` with a `reactivationToken`
125
+ - `SUSPENDED` → `ACCOUNT_SUSPENDED`
126
+ - 2FA on (and not a passkey)? → `SignInChallengesService.createChallenge` stores a short-lived challenge and the response is `two_factor_required` with a `challengeToken`. No tokens yet.
127
+ - Otherwise `startSignedInSession`:
128
+ - `SessionService.startSession` creates the `auth_sessions` row, then `issueTokens` signs the JWT access token (claims: user id `sub`, session id `sid`, roles, sign-in method `amr`...) and creates a refresh token (random, only its hash stored in `auth_refresh_tokens`).
129
+ - `UsersService.recordSignIn`, event `user.signed_in`, and a "new device" email if that setting is on.
130
+
131
+ ### Refresh: `POST /auth/refresh`
132
+
133
+ `AccountController.refresh` → **`SessionService.refreshSession`** (`features/sessions/session.service.ts`):
134
+
135
+ - The refresh token is marked used in **one UPDATE**, like link tokens. Exactly one of several parallel refreshes wins.
136
+ - No row came back? Either the token is unknown, or it was **already used**: `handleRefreshTokenReuse` treats that as possible theft, ends the whole session and records `session.refresh_token_reused`.
137
+ - The session must still be open and the account `ACTIVE`; then `issueTokens` returns a new pair. The old refresh token can never work again (rotation).
138
+
139
+ ### Logout
140
+
141
+ `AccountController.logout` → `SignInService.signOut` → `SessionService.revokeSession` sets `revoked_at`. The access token still verifies until it expires (a few minutes), except on `@CheckSession()` routes, which see the revoked session immediately.
142
+
143
+ ### Forgot / reset / change password
144
+
145
+ - `requestPasswordReset`: link token `reset_password`, same answer whether the email exists or not.
146
+ - `resetPassword`: `useLinkToken` → save the new hash → `SessionService.revokeAllSessions` (everyone signed out) → event and "password changed" email.
147
+ - `changePassword`: needs the current password; other devices are signed out, this one stays.
148
+
149
+ ---
150
+
151
+ ## Two-factor authentication
152
+
153
+ All in `features/two-factor/two-factor.service.ts`:
154
+
155
+ - **`startSetup`**: `createTotpSecret` (`common/crypto/totp.ts`), stored encrypted (`EncryptionService.encrypt`: AWS KMS or the local AES-GCM key), not yet active. Returns the `otpauth://` URL for the QR code.
156
+ - **`confirmSetup`**: the first code from the app must be right (`checkAuthenticatorCode`). Then 2FA turns on, 10 backup codes are created (only hashes stored), and other devices are signed out.
157
+ - **Sign-in**: `completeSignIn` returned a `challengeToken`. `POST /auth/2fa/verify` → **`verifySignInChallenge`**: `findOpenChallenge`, rate limit per user, then the code is checked as an authenticator code **or** a backup code. A code's 30-second time step is remembered (`last_used_time_step`), so the same code can't be replayed. Wrong code → `recordFailedAttempt` (the challenge dies after 5). Right code → `markChallengeUsed` → `SignInService.finishTwoFactorSignIn` → the same `startSignedInSession` as above.
158
+
159
+ What to notice: the TOTP maths (`common/crypto/totp.ts`) is small and readable. It's a good place to see RFC 6238 implemented in about 100 lines.
160
+
161
+ ---
162
+
163
+ ## Passwordless
164
+
165
+ All in `features/passwordless/passwordless-auth.service.ts`:
166
+
167
+ - **Email code**: `sendEmailCode` → `OneTimeCodesService.createCode` (`features/one-time-codes/`; refuses if a code went out less than `resendAfterSeconds` ago; stores a salted hash) → email. `verifyEmailCode`:
168
+ - For a new email, the sign-up details are checked **before** the code is used, so a missing name doesn't waste the code.
169
+ - `OneTimeCodesService.verifyCode` (5 wrong tries kill the code).
170
+ - `UsersService.removeUnprovenPassword`: if someone registered this email long ago with *their* password and never verified it, proving the email now removes that password (account pre-hijacking protection).
171
+ - New user created already verified, or existing user marked verified, then `completeSignIn`.
172
+ - **Magic link**: the same shape with a link token (`magic_sign_in`) instead of a code.
173
+ - **Phone code**: the same shape with channel `sms`, sent by `NotificationsService.sendSmsCode` through the app's `smsSender` (e.g. `integrations/msg91-sms-sender.ts`).
174
+
175
+ ---
176
+
177
+ ## Social sign-in
178
+
179
+ `SocialAuthService.signInWithProvider` (`features/social/social-auth.service.ts`):
180
+
181
+ - `SocialProvidersService.verifyCredential` (`social-providers.service.ts`) checks the token with the provider: Google and Apple ID tokens are verified locally against their public keys (fetched and cached by `common/crypto/remote-public-keys.ts`), checking the audience is one of your client ids; Facebook tokens are checked with the Graph API and your app secret.
182
+ - Then the decision described at the top of the file: already linked → sign in; provider proved the email and a user has it → link and sign in; email not proven but taken → refuse; nobody has it → create the user. Every branch ends in `completeSignIn`.
183
+
184
+ ---
185
+
186
+ ## Passkeys
187
+
188
+ `features/passkeys/passkeys.service.ts`, built on `@simplewebauthn/server`:
189
+
190
+ - **Adding one**: `startRegistration` creates WebAuthn options and stores the random challenge; the browser or phone creates a key pair and signs it; `finishRegistration` verifies the response and saves the public key (`auth_passkeys`).
191
+ - **Signing in**: `startSignIn` → the device signs the challenge with the private key (which never leaves it) → `finishSignIn` checks the signature with the stored public key and the signature counter, then `completeSignIn` with method `passkey`, which skips the 2FA step because the passkey already required a fingerprint, face or PIN.
192
+
193
+ ---
194
+
195
+ ## Email change
196
+
197
+ `features/email-change/email-change.service.ts`:
198
+
199
+ - `requestEmailChange`: `confirmOwner` (current password, or a sign-in within the last 10 minutes for accounts without one), then a `change_email` link token is created for the **new** address, unless another account already has that address. The answer is the same in both cases.
200
+ - `confirmEmailChange`: `useLinkToken` → update the email (the unique index catches a race with another account) → cancel old links → notice email to the **old** address → event → `activateIfReady`.
201
+
202
+ ---
203
+
204
+ ## Events, audit log and webhooks
205
+
206
+ Every important action calls **`AuthEventsService.record`** (`features/events/auth-events.service.ts`). One call:
207
+
208
+ - writes a row to `auth_audit_logs` (who, what, IP, device, details),
209
+ - calls the app's `onEvent` handler,
210
+ - inserts one `auth_webhook_deliveries` row (status `pending`) per webhook subscribed to that event.
211
+
212
+ A failure here is logged but never breaks the action that caused it.
213
+
214
+ **`WebhookDeliveryWorker.sendDueDeliveries`** (`features/webhooks/webhook-delivery.worker.ts`) runs every few seconds on every server:
215
+
216
+ - Claims due deliveries with `FOR UPDATE SKIP LOCKED`, so two servers never send the same delivery.
217
+ - Re-checks the URL is public (`common/network-safety.ts`) before each send, and never follows redirects (protection against requests to internal addresses).
218
+ - Signs the body with HMAC (`webhook-signature.ts`; the receiver checks it with `verifyWebhookSignature`).
219
+ - 2xx → `delivered`; anything else → retried with growing delays, then `failed`.
220
+
221
+ ---
222
+
223
+ ## Settings
224
+
225
+ `SettingsService.getSettings` (`features/settings/settings.service.ts`) builds the settings in force from three layers: `DEFAULT_SETTINGS` (constants.ts) ← the app's `settings` in code ← admin changes stored in `auth_settings`. The result is cached for 10 seconds per server, which is how an admin change reaches every server without a restart.
226
+
227
+ Every change goes through `settings.validation.ts`: field rules (types and ranges, e.g. `magicLink.expiresInMinutes` between 5 and 60) and `findSettingsConflicts` (combinations, e.g. not every sign-in method off).
228
+
229
+ To add a setting: add it to `AuthSettings` (`settings.types.ts`), give it a default in `DEFAULT_SETTINGS`, add a rule in `settings.validation.ts`, use `settings.yourSection.yourField` in the service, and document it in [Configuration](configuration.md#reference).
230
+
231
+ ---
232
+
233
+ ## Organizations
234
+
235
+ `features/organizations/organizations.service.ts`: members have a role (`owner`, `admin`, `member`) in `auth_organization_members`. `requireRole` and `assertMayManage` enforce who may do what (an admin can't touch owners; the last owner can't leave). Invitations are link-token-like rows in `auth_organization_invitations`. `switchActiveOrganization` stores the active organization on the session and issues a new access token with `org_id` and `org_role`.
236
+
237
+ ---
238
+
239
+ ## Background cleanup
240
+
241
+ `DataRetentionService` (`features/maintenance/data-retention.service.ts`) runs every hour. Each step in its `steps` list names a table and a setting; the SQL condition for "old" is in `DATA_RETENTION_OLD_ROW_CONDITION` (constants.ts). Rows are deleted 5,000 at a time so locks stay short.
242
+
243
+ ---
244
+
245
+ ## Where the security decisions are
246
+
247
+ Good places to slow down and read every comment:
248
+
249
+ | Topic | Where |
250
+ |---|---|
251
+ | No account enumeration | `register`, `requestPasswordReset`, `sendEmailCode`, `requestEmailChange` all answer the same either way |
252
+ | Timing-safe password check | `PasswordPolicyService.verifyPassword` (dummy hash) |
253
+ | Single-use links, codes, refresh tokens | the one-UPDATE pattern in `useLinkToken`, `verifyCode`, `refreshSession` |
254
+ | Refresh token theft detection | `SessionService.handleRefreshTokenReuse` |
255
+ | Account pre-hijacking | `UsersService.removeUnprovenPassword` |
256
+ | 2FA replay | `last_used_time_step` in `two-factor.service.ts` |
257
+ | Secrets at rest | `common/encryption.service.ts` (KMS or AES-GCM, with a context per record) |
258
+ | Webhook SSRF | `common/network-safety.ts` and the worker |
259
+ | Cookie mode CSRF | `common/refresh-token-cookie.ts` (`assertAllowedOrigin`) |
260
+
261
+ ---
262
+
263
+ ## Practice exercises
264
+
265
+ Small changes that force you through the code. Do them on a branch; the tests tell you if something broke.
266
+
267
+ - **Trace a refresh token reuse** in the debugger: refresh once, then send the *old* refresh token again. Watch `handleRefreshTokenReuse` end the session, and find the `session.refresh_token_reused` row in `auth_audit_logs`.
268
+ - **Add a setting** `security.notifyOnPasswordReset` following the recipe in "Settings" above.
269
+ - **Add an event**: record `user.backup_codes_regenerated` in `TwoFactorService.regenerateBackupCodes` (add it to `AUTH_EVENTS` and to [Events](events.md)).
270
+ - **Write a test** in `test/` for `buildRefreshCookie` (`common/refresh-token-cookie.ts`) checking every cookie attribute.
271
+ - **Read a test**: `scripts/e2e-check.js` and `scripts/http-flow-check.js` are executable descriptions of every flow; reading them next to this page shows what each flow must guarantee.
272
+
273
+ Before opening a pull request: `npm run typecheck && npm test && npm run build`, and see [Architecture](architecture.md#design-rules-for-contributors) for the house rules.