@portaidentity/server 1.7.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 (742) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +33 -0
  4. package/dist/applications/cache.d.ts +64 -0
  5. package/dist/applications/cache.d.ts.map +1 -0
  6. package/dist/applications/cache.js +161 -0
  7. package/dist/applications/cache.js.map +1 -0
  8. package/dist/applications/errors.d.ts +25 -0
  9. package/dist/applications/errors.d.ts.map +1 -0
  10. package/dist/applications/errors.js +31 -0
  11. package/dist/applications/errors.js.map +1 -0
  12. package/dist/applications/index.d.ts +17 -0
  13. package/dist/applications/index.d.ts.map +1 -0
  14. package/dist/applications/index.js +19 -0
  15. package/dist/applications/index.js.map +1 -0
  16. package/dist/applications/repository.d.ts +184 -0
  17. package/dist/applications/repository.d.ts.map +1 -0
  18. package/dist/applications/repository.js +367 -0
  19. package/dist/applications/repository.js.map +1 -0
  20. package/dist/applications/service.d.ts +160 -0
  21. package/dist/applications/service.d.ts.map +1 -0
  22. package/dist/applications/service.js +388 -0
  23. package/dist/applications/service.js.map +1 -0
  24. package/dist/applications/slugs.d.ts +61 -0
  25. package/dist/applications/slugs.d.ts.map +1 -0
  26. package/dist/applications/slugs.js +115 -0
  27. package/dist/applications/slugs.js.map +1 -0
  28. package/dist/applications/types.d.ts +129 -0
  29. package/dist/applications/types.d.ts.map +1 -0
  30. package/dist/applications/types.js +62 -0
  31. package/dist/applications/types.js.map +1 -0
  32. package/dist/auth/csrf.d.ts +71 -0
  33. package/dist/auth/csrf.d.ts.map +1 -0
  34. package/dist/auth/csrf.js +112 -0
  35. package/dist/auth/csrf.js.map +1 -0
  36. package/dist/auth/email-renderer.d.ts +37 -0
  37. package/dist/auth/email-renderer.d.ts.map +1 -0
  38. package/dist/auth/email-renderer.js +116 -0
  39. package/dist/auth/email-renderer.js.map +1 -0
  40. package/dist/auth/email-service.d.ts +154 -0
  41. package/dist/auth/email-service.d.ts.map +1 -0
  42. package/dist/auth/email-service.js +415 -0
  43. package/dist/auth/email-service.js.map +1 -0
  44. package/dist/auth/email-transport.d.ts +65 -0
  45. package/dist/auth/email-transport.d.ts.map +1 -0
  46. package/dist/auth/email-transport.js +82 -0
  47. package/dist/auth/email-transport.js.map +1 -0
  48. package/dist/auth/i18n.d.ts +81 -0
  49. package/dist/auth/i18n.d.ts.map +1 -0
  50. package/dist/auth/i18n.js +395 -0
  51. package/dist/auth/i18n.js.map +1 -0
  52. package/dist/auth/index.d.ts +28 -0
  53. package/dist/auth/index.d.ts.map +1 -0
  54. package/dist/auth/index.js +31 -0
  55. package/dist/auth/index.js.map +1 -0
  56. package/dist/auth/interaction-authority.d.ts +39 -0
  57. package/dist/auth/interaction-authority.d.ts.map +1 -0
  58. package/dist/auth/interaction-authority.js +30 -0
  59. package/dist/auth/interaction-authority.js.map +1 -0
  60. package/dist/auth/magic-link-session.d.ts +81 -0
  61. package/dist/auth/magic-link-session.d.ts.map +1 -0
  62. package/dist/auth/magic-link-session.js +183 -0
  63. package/dist/auth/magic-link-session.js.map +1 -0
  64. package/dist/auth/rate-limiter.d.ts +143 -0
  65. package/dist/auth/rate-limiter.d.ts.map +1 -0
  66. package/dist/auth/rate-limiter.js +220 -0
  67. package/dist/auth/rate-limiter.js.map +1 -0
  68. package/dist/auth/recovery-crypto.d.ts +34 -0
  69. package/dist/auth/recovery-crypto.d.ts.map +1 -0
  70. package/dist/auth/recovery-crypto.js +103 -0
  71. package/dist/auth/recovery-crypto.js.map +1 -0
  72. package/dist/auth/recovery-job-processor.d.ts +24 -0
  73. package/dist/auth/recovery-job-processor.d.ts.map +1 -0
  74. package/dist/auth/recovery-job-processor.js +123 -0
  75. package/dist/auth/recovery-job-processor.js.map +1 -0
  76. package/dist/auth/recovery-job-repository.d.ts +219 -0
  77. package/dist/auth/recovery-job-repository.d.ts.map +1 -0
  78. package/dist/auth/recovery-job-repository.js +340 -0
  79. package/dist/auth/recovery-job-repository.js.map +1 -0
  80. package/dist/auth/recovery-service.d.ts +27 -0
  81. package/dist/auth/recovery-service.d.ts.map +1 -0
  82. package/dist/auth/recovery-service.js +53 -0
  83. package/dist/auth/recovery-service.js.map +1 -0
  84. package/dist/auth/recovery-worker.d.ts +158 -0
  85. package/dist/auth/recovery-worker.d.ts.map +1 -0
  86. package/dist/auth/recovery-worker.js +306 -0
  87. package/dist/auth/recovery-worker.js.map +1 -0
  88. package/dist/auth/template-engine.d.ts +134 -0
  89. package/dist/auth/template-engine.d.ts.map +1 -0
  90. package/dist/auth/template-engine.js +225 -0
  91. package/dist/auth/template-engine.js.map +1 -0
  92. package/dist/auth/token-repository.d.ts +238 -0
  93. package/dist/auth/token-repository.d.ts.map +1 -0
  94. package/dist/auth/token-repository.js +475 -0
  95. package/dist/auth/token-repository.js.map +1 -0
  96. package/dist/auth/tokens.d.ts +45 -0
  97. package/dist/auth/tokens.d.ts.map +1 -0
  98. package/dist/auth/tokens.js +51 -0
  99. package/dist/auth/tokens.js.map +1 -0
  100. package/dist/cli/bootstrap.d.ts +55 -0
  101. package/dist/cli/bootstrap.d.ts.map +1 -0
  102. package/dist/cli/bootstrap.js +90 -0
  103. package/dist/cli/bootstrap.js.map +1 -0
  104. package/dist/cli/commands/health.d.ts +20 -0
  105. package/dist/cli/commands/health.d.ts.map +1 -0
  106. package/dist/cli/commands/health.js +80 -0
  107. package/dist/cli/commands/health.js.map +1 -0
  108. package/dist/cli/commands/init.d.ts +48 -0
  109. package/dist/cli/commands/init.d.ts.map +1 -0
  110. package/dist/cli/commands/init.js +349 -0
  111. package/dist/cli/commands/init.js.map +1 -0
  112. package/dist/cli/commands/migrate.d.ts +19 -0
  113. package/dist/cli/commands/migrate.d.ts.map +1 -0
  114. package/dist/cli/commands/migrate.js +88 -0
  115. package/dist/cli/commands/migrate.js.map +1 -0
  116. package/dist/cli/commands/seed.d.ts +19 -0
  117. package/dist/cli/commands/seed.d.ts.map +1 -0
  118. package/dist/cli/commands/seed.js +71 -0
  119. package/dist/cli/commands/seed.js.map +1 -0
  120. package/dist/cli/commands/user-2fa.d.ts +29 -0
  121. package/dist/cli/commands/user-2fa.d.ts.map +1 -0
  122. package/dist/cli/commands/user-2fa.js +113 -0
  123. package/dist/cli/commands/user-2fa.js.map +1 -0
  124. package/dist/cli/error-handler.d.ts +31 -0
  125. package/dist/cli/error-handler.d.ts.map +1 -0
  126. package/dist/cli/error-handler.js +65 -0
  127. package/dist/cli/error-handler.js.map +1 -0
  128. package/dist/cli/index.d.ts +48 -0
  129. package/dist/cli/index.d.ts.map +1 -0
  130. package/dist/cli/index.js +99 -0
  131. package/dist/cli/index.js.map +1 -0
  132. package/dist/cli/output.d.ts +94 -0
  133. package/dist/cli/output.d.ts.map +1 -0
  134. package/dist/cli/output.js +124 -0
  135. package/dist/cli/output.js.map +1 -0
  136. package/dist/cli/prompt.d.ts +48 -0
  137. package/dist/cli/prompt.d.ts.map +1 -0
  138. package/dist/cli/prompt.js +96 -0
  139. package/dist/cli/prompt.js.map +1 -0
  140. package/dist/clients/cache.d.ts +63 -0
  141. package/dist/clients/cache.d.ts.map +1 -0
  142. package/dist/clients/cache.js +160 -0
  143. package/dist/clients/cache.js.map +1 -0
  144. package/dist/clients/crypto.d.ts +69 -0
  145. package/dist/clients/crypto.d.ts.map +1 -0
  146. package/dist/clients/crypto.js +83 -0
  147. package/dist/clients/crypto.js.map +1 -0
  148. package/dist/clients/errors.d.ts +27 -0
  149. package/dist/clients/errors.d.ts.map +1 -0
  150. package/dist/clients/errors.js +33 -0
  151. package/dist/clients/errors.js.map +1 -0
  152. package/dist/clients/index.d.ts +19 -0
  153. package/dist/clients/index.d.ts.map +1 -0
  154. package/dist/clients/index.js +23 -0
  155. package/dist/clients/index.js.map +1 -0
  156. package/dist/clients/repository.d.ts +161 -0
  157. package/dist/clients/repository.d.ts.map +1 -0
  158. package/dist/clients/repository.js +318 -0
  159. package/dist/clients/repository.js.map +1 -0
  160. package/dist/clients/resolve-login-methods.d.ts +74 -0
  161. package/dist/clients/resolve-login-methods.d.ts.map +1 -0
  162. package/dist/clients/resolve-login-methods.js +68 -0
  163. package/dist/clients/resolve-login-methods.js.map +1 -0
  164. package/dist/clients/secret-repository.d.ts +109 -0
  165. package/dist/clients/secret-repository.d.ts.map +1 -0
  166. package/dist/clients/secret-repository.js +166 -0
  167. package/dist/clients/secret-repository.js.map +1 -0
  168. package/dist/clients/secret-service.d.ts +87 -0
  169. package/dist/clients/secret-service.d.ts.map +1 -0
  170. package/dist/clients/secret-service.js +204 -0
  171. package/dist/clients/secret-service.js.map +1 -0
  172. package/dist/clients/service.d.ts +158 -0
  173. package/dist/clients/service.d.ts.map +1 -0
  174. package/dist/clients/service.js +523 -0
  175. package/dist/clients/service.js.map +1 -0
  176. package/dist/clients/types.d.ts +214 -0
  177. package/dist/clients/types.d.ts.map +1 -0
  178. package/dist/clients/types.js +77 -0
  179. package/dist/clients/types.js.map +1 -0
  180. package/dist/clients/validators.d.ts +87 -0
  181. package/dist/clients/validators.d.ts.map +1 -0
  182. package/dist/clients/validators.js +164 -0
  183. package/dist/clients/validators.js.map +1 -0
  184. package/dist/config/index.d.ts +23 -0
  185. package/dist/config/index.d.ts.map +1 -0
  186. package/dist/config/index.js +52 -0
  187. package/dist/config/index.js.map +1 -0
  188. package/dist/config/schema.d.ts +35 -0
  189. package/dist/config/schema.d.ts.map +1 -0
  190. package/dist/config/schema.js +167 -0
  191. package/dist/config/schema.js.map +1 -0
  192. package/dist/custom-claims/cache.d.ts +48 -0
  193. package/dist/custom-claims/cache.d.ts.map +1 -0
  194. package/dist/custom-claims/cache.js +122 -0
  195. package/dist/custom-claims/cache.js.map +1 -0
  196. package/dist/custom-claims/errors.d.ts +25 -0
  197. package/dist/custom-claims/errors.d.ts.map +1 -0
  198. package/dist/custom-claims/errors.js +31 -0
  199. package/dist/custom-claims/errors.js.map +1 -0
  200. package/dist/custom-claims/index.d.ts +14 -0
  201. package/dist/custom-claims/index.d.ts.map +1 -0
  202. package/dist/custom-claims/index.js +17 -0
  203. package/dist/custom-claims/index.js.map +1 -0
  204. package/dist/custom-claims/repository.d.ts +147 -0
  205. package/dist/custom-claims/repository.d.ts.map +1 -0
  206. package/dist/custom-claims/repository.js +338 -0
  207. package/dist/custom-claims/repository.js.map +1 -0
  208. package/dist/custom-claims/service.d.ts +129 -0
  209. package/dist/custom-claims/service.d.ts.map +1 -0
  210. package/dist/custom-claims/service.js +296 -0
  211. package/dist/custom-claims/service.js.map +1 -0
  212. package/dist/custom-claims/types.d.ts +132 -0
  213. package/dist/custom-claims/types.d.ts.map +1 -0
  214. package/dist/custom-claims/types.js +61 -0
  215. package/dist/custom-claims/types.js.map +1 -0
  216. package/dist/custom-claims/validators.d.ts +61 -0
  217. package/dist/custom-claims/validators.d.ts.map +1 -0
  218. package/dist/custom-claims/validators.js +136 -0
  219. package/dist/custom-claims/validators.js.map +1 -0
  220. package/dist/index.d.ts +14 -0
  221. package/dist/index.d.ts.map +1 -0
  222. package/dist/index.js +91 -0
  223. package/dist/index.js.map +1 -0
  224. package/dist/lib/admin-permissions.d.ts +147 -0
  225. package/dist/lib/admin-permissions.d.ts.map +1 -0
  226. package/dist/lib/admin-permissions.js +261 -0
  227. package/dist/lib/admin-permissions.js.map +1 -0
  228. package/dist/lib/audit-log.d.ts +45 -0
  229. package/dist/lib/audit-log.d.ts.map +1 -0
  230. package/dist/lib/audit-log.js +79 -0
  231. package/dist/lib/audit-log.js.map +1 -0
  232. package/dist/lib/branding-assets.d.ts +47 -0
  233. package/dist/lib/branding-assets.d.ts.map +1 -0
  234. package/dist/lib/branding-assets.js +93 -0
  235. package/dist/lib/branding-assets.js.map +1 -0
  236. package/dist/lib/bulk-operations.d.ts +54 -0
  237. package/dist/lib/bulk-operations.d.ts.map +1 -0
  238. package/dist/lib/bulk-operations.js +201 -0
  239. package/dist/lib/bulk-operations.js.map +1 -0
  240. package/dist/lib/cursor.d.ts +110 -0
  241. package/dist/lib/cursor.d.ts.map +1 -0
  242. package/dist/lib/cursor.js +115 -0
  243. package/dist/lib/cursor.js.map +1 -0
  244. package/dist/lib/data-export.d.ts +67 -0
  245. package/dist/lib/data-export.d.ts.map +1 -0
  246. package/dist/lib/data-export.js +272 -0
  247. package/dist/lib/data-export.js.map +1 -0
  248. package/dist/lib/data-import-plan.d.ts +50 -0
  249. package/dist/lib/data-import-plan.d.ts.map +1 -0
  250. package/dist/lib/data-import-plan.js +235 -0
  251. package/dist/lib/data-import-plan.js.map +1 -0
  252. package/dist/lib/data-import.d.ts +276 -0
  253. package/dist/lib/data-import.d.ts.map +1 -0
  254. package/dist/lib/data-import.js +1694 -0
  255. package/dist/lib/data-import.js.map +1 -0
  256. package/dist/lib/database.d.ts +22 -0
  257. package/dist/lib/database.d.ts.map +1 -0
  258. package/dist/lib/database.js +103 -0
  259. package/dist/lib/database.js.map +1 -0
  260. package/dist/lib/entity-history.d.ts +52 -0
  261. package/dist/lib/entity-history.d.ts.map +1 -0
  262. package/dist/lib/entity-history.js +121 -0
  263. package/dist/lib/entity-history.js.map +1 -0
  264. package/dist/lib/etag.d.ts +92 -0
  265. package/dist/lib/etag.d.ts.map +1 -0
  266. package/dist/lib/etag.js +155 -0
  267. package/dist/lib/etag.js.map +1 -0
  268. package/dist/lib/image-validator.d.ts +44 -0
  269. package/dist/lib/image-validator.d.ts.map +1 -0
  270. package/dist/lib/image-validator.js +204 -0
  271. package/dist/lib/image-validator.js.map +1 -0
  272. package/dist/lib/logger.d.ts +15 -0
  273. package/dist/lib/logger.d.ts.map +1 -0
  274. package/dist/lib/logger.js +84 -0
  275. package/dist/lib/logger.js.map +1 -0
  276. package/dist/lib/migrator.d.ts +14 -0
  277. package/dist/lib/migrator.d.ts.map +1 -0
  278. package/dist/lib/migrator.js +34 -0
  279. package/dist/lib/migrator.js.map +1 -0
  280. package/dist/lib/redis.d.ts +5 -0
  281. package/dist/lib/redis.d.ts.map +1 -0
  282. package/dist/lib/redis.js +29 -0
  283. package/dist/lib/redis.js.map +1 -0
  284. package/dist/lib/runtime-paths.d.ts +38 -0
  285. package/dist/lib/runtime-paths.d.ts.map +1 -0
  286. package/dist/lib/runtime-paths.js +50 -0
  287. package/dist/lib/runtime-paths.js.map +1 -0
  288. package/dist/lib/session-tracking.d.ts +81 -0
  289. package/dist/lib/session-tracking.d.ts.map +1 -0
  290. package/dist/lib/session-tracking.js +145 -0
  291. package/dist/lib/session-tracking.js.map +1 -0
  292. package/dist/lib/signing-key-crypto.d.ts +53 -0
  293. package/dist/lib/signing-key-crypto.d.ts.map +1 -0
  294. package/dist/lib/signing-key-crypto.js +108 -0
  295. package/dist/lib/signing-key-crypto.js.map +1 -0
  296. package/dist/lib/signing-keys.d.ts +141 -0
  297. package/dist/lib/signing-keys.d.ts.map +1 -0
  298. package/dist/lib/signing-keys.js +206 -0
  299. package/dist/lib/signing-keys.js.map +1 -0
  300. package/dist/lib/stats.d.ts +83 -0
  301. package/dist/lib/stats.d.ts.map +1 -0
  302. package/dist/lib/stats.js +187 -0
  303. package/dist/lib/stats.js.map +1 -0
  304. package/dist/lib/super-admin-protection.d.ts +69 -0
  305. package/dist/lib/super-admin-protection.d.ts.map +1 -0
  306. package/dist/lib/super-admin-protection.js +102 -0
  307. package/dist/lib/super-admin-protection.js.map +1 -0
  308. package/dist/lib/system-config.d.ts +79 -0
  309. package/dist/lib/system-config.d.ts.map +1 -0
  310. package/dist/lib/system-config.js +159 -0
  311. package/dist/lib/system-config.js.map +1 -0
  312. package/dist/middleware/admin-auth.d.ts +100 -0
  313. package/dist/middleware/admin-auth.d.ts.map +1 -0
  314. package/dist/middleware/admin-auth.js +241 -0
  315. package/dist/middleware/admin-auth.js.map +1 -0
  316. package/dist/middleware/admin-cors.d.ts +41 -0
  317. package/dist/middleware/admin-cors.d.ts.map +1 -0
  318. package/dist/middleware/admin-cors.js +83 -0
  319. package/dist/middleware/admin-cors.js.map +1 -0
  320. package/dist/middleware/admin-mutation-audit.d.ts +9 -0
  321. package/dist/middleware/admin-mutation-audit.d.ts.map +1 -0
  322. package/dist/middleware/admin-mutation-audit.js +76 -0
  323. package/dist/middleware/admin-mutation-audit.js.map +1 -0
  324. package/dist/middleware/admin-rate-limiter.d.ts +56 -0
  325. package/dist/middleware/admin-rate-limiter.d.ts.map +1 -0
  326. package/dist/middleware/admin-rate-limiter.js +97 -0
  327. package/dist/middleware/admin-rate-limiter.js.map +1 -0
  328. package/dist/middleware/client-secret-hash.d.ts +34 -0
  329. package/dist/middleware/client-secret-hash.d.ts.map +1 -0
  330. package/dist/middleware/client-secret-hash.js +74 -0
  331. package/dist/middleware/client-secret-hash.js.map +1 -0
  332. package/dist/middleware/error-handler.d.ts +3 -0
  333. package/dist/middleware/error-handler.d.ts.map +1 -0
  334. package/dist/middleware/error-handler.js +93 -0
  335. package/dist/middleware/error-handler.js.map +1 -0
  336. package/dist/middleware/health.d.ts +3 -0
  337. package/dist/middleware/health.d.ts.map +1 -0
  338. package/dist/middleware/health.js +35 -0
  339. package/dist/middleware/health.js.map +1 -0
  340. package/dist/middleware/metrics.d.ts +40 -0
  341. package/dist/middleware/metrics.d.ts.map +1 -0
  342. package/dist/middleware/metrics.js +75 -0
  343. package/dist/middleware/metrics.js.map +1 -0
  344. package/dist/middleware/oidc-client-tenant.d.ts +37 -0
  345. package/dist/middleware/oidc-client-tenant.d.ts.map +1 -0
  346. package/dist/middleware/oidc-client-tenant.js +156 -0
  347. package/dist/middleware/oidc-client-tenant.js.map +1 -0
  348. package/dist/middleware/oidc-cors.d.ts +33 -0
  349. package/dist/middleware/oidc-cors.d.ts.map +1 -0
  350. package/dist/middleware/oidc-cors.js +68 -0
  351. package/dist/middleware/oidc-cors.js.map +1 -0
  352. package/dist/middleware/oidc-preflight-cors.d.ts +54 -0
  353. package/dist/middleware/oidc-preflight-cors.d.ts.map +1 -0
  354. package/dist/middleware/oidc-preflight-cors.js +267 -0
  355. package/dist/middleware/oidc-preflight-cors.js.map +1 -0
  356. package/dist/middleware/prompt-login-reset.d.ts +103 -0
  357. package/dist/middleware/prompt-login-reset.d.ts.map +1 -0
  358. package/dist/middleware/prompt-login-reset.js +204 -0
  359. package/dist/middleware/prompt-login-reset.js.map +1 -0
  360. package/dist/middleware/ready.d.ts +23 -0
  361. package/dist/middleware/ready.d.ts.map +1 -0
  362. package/dist/middleware/ready.js +60 -0
  363. package/dist/middleware/ready.js.map +1 -0
  364. package/dist/middleware/request-logger.d.ts +27 -0
  365. package/dist/middleware/request-logger.d.ts.map +1 -0
  366. package/dist/middleware/request-logger.js +46 -0
  367. package/dist/middleware/request-logger.js.map +1 -0
  368. package/dist/middleware/require-permission.d.ts +28 -0
  369. package/dist/middleware/require-permission.d.ts.map +1 -0
  370. package/dist/middleware/require-permission.js +63 -0
  371. package/dist/middleware/require-permission.js.map +1 -0
  372. package/dist/middleware/require-user-organization.d.ts +18 -0
  373. package/dist/middleware/require-user-organization.d.ts.map +1 -0
  374. package/dist/middleware/require-user-organization.js +53 -0
  375. package/dist/middleware/require-user-organization.js.map +1 -0
  376. package/dist/middleware/root-page.d.ts +62 -0
  377. package/dist/middleware/root-page.d.ts.map +1 -0
  378. package/dist/middleware/root-page.js +134 -0
  379. package/dist/middleware/root-page.js.map +1 -0
  380. package/dist/middleware/security-headers.d.ts +98 -0
  381. package/dist/middleware/security-headers.d.ts.map +1 -0
  382. package/dist/middleware/security-headers.js +159 -0
  383. package/dist/middleware/security-headers.js.map +1 -0
  384. package/dist/middleware/tenant-resolver.d.ts +37 -0
  385. package/dist/middleware/tenant-resolver.d.ts.map +1 -0
  386. package/dist/middleware/tenant-resolver.js +71 -0
  387. package/dist/middleware/tenant-resolver.js.map +1 -0
  388. package/dist/middleware/token-rate-limiter.d.ts +89 -0
  389. package/dist/middleware/token-rate-limiter.d.ts.map +1 -0
  390. package/dist/middleware/token-rate-limiter.js +189 -0
  391. package/dist/middleware/token-rate-limiter.js.map +1 -0
  392. package/dist/oidc/account-finder.d.ts +52 -0
  393. package/dist/oidc/account-finder.d.ts.map +1 -0
  394. package/dist/oidc/account-finder.js +134 -0
  395. package/dist/oidc/account-finder.js.map +1 -0
  396. package/dist/oidc/adapter-factory.d.ts +90 -0
  397. package/dist/oidc/adapter-factory.d.ts.map +1 -0
  398. package/dist/oidc/adapter-factory.js +165 -0
  399. package/dist/oidc/adapter-factory.js.map +1 -0
  400. package/dist/oidc/configuration.d.ts +155 -0
  401. package/dist/oidc/configuration.d.ts.map +1 -0
  402. package/dist/oidc/configuration.js +531 -0
  403. package/dist/oidc/configuration.js.map +1 -0
  404. package/dist/oidc/postgres-adapter.d.ts +159 -0
  405. package/dist/oidc/postgres-adapter.d.ts.map +1 -0
  406. package/dist/oidc/postgres-adapter.js +222 -0
  407. package/dist/oidc/postgres-adapter.js.map +1 -0
  408. package/dist/oidc/protocol-security-observer.d.ts +38 -0
  409. package/dist/oidc/protocol-security-observer.d.ts.map +1 -0
  410. package/dist/oidc/protocol-security-observer.js +93 -0
  411. package/dist/oidc/protocol-security-observer.js.map +1 -0
  412. package/dist/oidc/provider.d.ts +37 -0
  413. package/dist/oidc/provider.d.ts.map +1 -0
  414. package/dist/oidc/provider.js +71 -0
  415. package/dist/oidc/provider.js.map +1 -0
  416. package/dist/oidc/redis-adapter.d.ts +140 -0
  417. package/dist/oidc/redis-adapter.d.ts.map +1 -0
  418. package/dist/oidc/redis-adapter.js +344 -0
  419. package/dist/oidc/redis-adapter.js.map +1 -0
  420. package/dist/organizations/cache.d.ts +60 -0
  421. package/dist/organizations/cache.d.ts.map +1 -0
  422. package/dist/organizations/cache.js +157 -0
  423. package/dist/organizations/cache.js.map +1 -0
  424. package/dist/organizations/errors.d.ts +25 -0
  425. package/dist/organizations/errors.d.ts.map +1 -0
  426. package/dist/organizations/errors.js +31 -0
  427. package/dist/organizations/errors.js.map +1 -0
  428. package/dist/organizations/index.d.ts +15 -0
  429. package/dist/organizations/index.d.ts.map +1 -0
  430. package/dist/organizations/index.js +15 -0
  431. package/dist/organizations/index.js.map +1 -0
  432. package/dist/organizations/repository.d.ts +183 -0
  433. package/dist/organizations/repository.d.ts.map +1 -0
  434. package/dist/organizations/repository.js +354 -0
  435. package/dist/organizations/repository.js.map +1 -0
  436. package/dist/organizations/service.d.ts +168 -0
  437. package/dist/organizations/service.d.ts.map +1 -0
  438. package/dist/organizations/service.js +511 -0
  439. package/dist/organizations/service.js.map +1 -0
  440. package/dist/organizations/slugs.d.ts +57 -0
  441. package/dist/organizations/slugs.d.ts.map +1 -0
  442. package/dist/organizations/slugs.js +130 -0
  443. package/dist/organizations/slugs.js.map +1 -0
  444. package/dist/organizations/types.d.ts +146 -0
  445. package/dist/organizations/types.d.ts.map +1 -0
  446. package/dist/organizations/types.js +49 -0
  447. package/dist/organizations/types.js.map +1 -0
  448. package/dist/rbac/cache.d.ts +110 -0
  449. package/dist/rbac/cache.d.ts.map +1 -0
  450. package/dist/rbac/cache.js +271 -0
  451. package/dist/rbac/cache.js.map +1 -0
  452. package/dist/rbac/errors.d.ts +34 -0
  453. package/dist/rbac/errors.d.ts.map +1 -0
  454. package/dist/rbac/errors.js +43 -0
  455. package/dist/rbac/errors.js.map +1 -0
  456. package/dist/rbac/index.d.ts +17 -0
  457. package/dist/rbac/index.d.ts.map +1 -0
  458. package/dist/rbac/index.js +21 -0
  459. package/dist/rbac/index.js.map +1 -0
  460. package/dist/rbac/mapping-repository.d.ts +121 -0
  461. package/dist/rbac/mapping-repository.d.ts.map +1 -0
  462. package/dist/rbac/mapping-repository.js +224 -0
  463. package/dist/rbac/mapping-repository.js.map +1 -0
  464. package/dist/rbac/permission-repository.d.ts +100 -0
  465. package/dist/rbac/permission-repository.d.ts.map +1 -0
  466. package/dist/rbac/permission-repository.js +190 -0
  467. package/dist/rbac/permission-repository.js.map +1 -0
  468. package/dist/rbac/permission-service.d.ts +85 -0
  469. package/dist/rbac/permission-service.d.ts.map +1 -0
  470. package/dist/rbac/permission-service.js +179 -0
  471. package/dist/rbac/permission-service.js.map +1 -0
  472. package/dist/rbac/role-repository.d.ts +104 -0
  473. package/dist/rbac/role-repository.d.ts.map +1 -0
  474. package/dist/rbac/role-repository.js +202 -0
  475. package/dist/rbac/role-repository.js.map +1 -0
  476. package/dist/rbac/role-service.d.ts +111 -0
  477. package/dist/rbac/role-service.d.ts.map +1 -0
  478. package/dist/rbac/role-service.js +256 -0
  479. package/dist/rbac/role-service.js.map +1 -0
  480. package/dist/rbac/slugs.d.ts +116 -0
  481. package/dist/rbac/slugs.d.ts.map +1 -0
  482. package/dist/rbac/slugs.js +178 -0
  483. package/dist/rbac/slugs.js.map +1 -0
  484. package/dist/rbac/types.d.ts +187 -0
  485. package/dist/rbac/types.d.ts.map +1 -0
  486. package/dist/rbac/types.js +75 -0
  487. package/dist/rbac/types.js.map +1 -0
  488. package/dist/rbac/user-role-service.d.ts +101 -0
  489. package/dist/rbac/user-role-service.d.ts.map +1 -0
  490. package/dist/rbac/user-role-service.js +166 -0
  491. package/dist/rbac/user-role-service.js.map +1 -0
  492. package/dist/routes/applications.d.ts +40 -0
  493. package/dist/routes/applications.d.ts.map +1 -0
  494. package/dist/routes/applications.js +284 -0
  495. package/dist/routes/applications.js.map +1 -0
  496. package/dist/routes/audit.d.ts +31 -0
  497. package/dist/routes/audit.d.ts.map +1 -0
  498. package/dist/routes/audit.js +140 -0
  499. package/dist/routes/audit.js.map +1 -0
  500. package/dist/routes/branding.d.ts +17 -0
  501. package/dist/routes/branding.d.ts.map +1 -0
  502. package/dist/routes/branding.js +104 -0
  503. package/dist/routes/branding.js.map +1 -0
  504. package/dist/routes/bulk.d.ts +37 -0
  505. package/dist/routes/bulk.d.ts.map +1 -0
  506. package/dist/routes/bulk.js +94 -0
  507. package/dist/routes/bulk.js.map +1 -0
  508. package/dist/routes/clients.d.ts +44 -0
  509. package/dist/routes/clients.d.ts.map +1 -0
  510. package/dist/routes/clients.js +353 -0
  511. package/dist/routes/clients.js.map +1 -0
  512. package/dist/routes/config.d.ts +35 -0
  513. package/dist/routes/config.d.ts.map +1 -0
  514. package/dist/routes/config.js +115 -0
  515. package/dist/routes/config.js.map +1 -0
  516. package/dist/routes/custom-claims.d.ts +38 -0
  517. package/dist/routes/custom-claims.d.ts.map +1 -0
  518. package/dist/routes/custom-claims.js +208 -0
  519. package/dist/routes/custom-claims.js.map +1 -0
  520. package/dist/routes/exports.d.ts +21 -0
  521. package/dist/routes/exports.d.ts.map +1 -0
  522. package/dist/routes/exports.js +98 -0
  523. package/dist/routes/exports.js.map +1 -0
  524. package/dist/routes/imports.d.ts +18 -0
  525. package/dist/routes/imports.d.ts.map +1 -0
  526. package/dist/routes/imports.js +72 -0
  527. package/dist/routes/imports.js.map +1 -0
  528. package/dist/routes/interactions.d.ts +111 -0
  529. package/dist/routes/interactions.d.ts.map +1 -0
  530. package/dist/routes/interactions.js +1074 -0
  531. package/dist/routes/interactions.js.map +1 -0
  532. package/dist/routes/invitation.d.ts +31 -0
  533. package/dist/routes/invitation.d.ts.map +1 -0
  534. package/dist/routes/invitation.js +342 -0
  535. package/dist/routes/invitation.js.map +1 -0
  536. package/dist/routes/keys.d.ts +28 -0
  537. package/dist/routes/keys.d.ts.map +1 -0
  538. package/dist/routes/keys.js +91 -0
  539. package/dist/routes/keys.js.map +1 -0
  540. package/dist/routes/magic-link.d.ts +43 -0
  541. package/dist/routes/magic-link.d.ts.map +1 -0
  542. package/dist/routes/magic-link.js +264 -0
  543. package/dist/routes/magic-link.js.map +1 -0
  544. package/dist/routes/organizations.d.ts +38 -0
  545. package/dist/routes/organizations.d.ts.map +1 -0
  546. package/dist/routes/organizations.js +347 -0
  547. package/dist/routes/organizations.js.map +1 -0
  548. package/dist/routes/password-reset.d.ts +65 -0
  549. package/dist/routes/password-reset.d.ts.map +1 -0
  550. package/dist/routes/password-reset.js +493 -0
  551. package/dist/routes/password-reset.js.map +1 -0
  552. package/dist/routes/permissions.d.ts +33 -0
  553. package/dist/routes/permissions.d.ts.map +1 -0
  554. package/dist/routes/permissions.js +166 -0
  555. package/dist/routes/permissions.js.map +1 -0
  556. package/dist/routes/roles.d.ts +37 -0
  557. package/dist/routes/roles.d.ts.map +1 -0
  558. package/dist/routes/roles.js +222 -0
  559. package/dist/routes/roles.js.map +1 -0
  560. package/dist/routes/sessions.d.ts +37 -0
  561. package/dist/routes/sessions.d.ts.map +1 -0
  562. package/dist/routes/sessions.js +121 -0
  563. package/dist/routes/sessions.js.map +1 -0
  564. package/dist/routes/stats.d.ts +28 -0
  565. package/dist/routes/stats.d.ts.map +1 -0
  566. package/dist/routes/stats.js +65 -0
  567. package/dist/routes/stats.js.map +1 -0
  568. package/dist/routes/two-factor-admin.d.ts +54 -0
  569. package/dist/routes/two-factor-admin.d.ts.map +1 -0
  570. package/dist/routes/two-factor-admin.js +344 -0
  571. package/dist/routes/two-factor-admin.js.map +1 -0
  572. package/dist/routes/two-factor.d.ts +31 -0
  573. package/dist/routes/two-factor.d.ts.map +1 -0
  574. package/dist/routes/two-factor.js +595 -0
  575. package/dist/routes/two-factor.js.map +1 -0
  576. package/dist/routes/user-roles.d.ts +33 -0
  577. package/dist/routes/user-roles.d.ts.map +1 -0
  578. package/dist/routes/user-roles.js +129 -0
  579. package/dist/routes/user-roles.js.map +1 -0
  580. package/dist/routes/users.d.ts +61 -0
  581. package/dist/routes/users.d.ts.map +1 -0
  582. package/dist/routes/users.js +868 -0
  583. package/dist/routes/users.js.map +1 -0
  584. package/dist/security/decision-context.d.ts +52 -0
  585. package/dist/security/decision-context.d.ts.map +1 -0
  586. package/dist/security/decision-context.js +150 -0
  587. package/dist/security/decision-context.js.map +1 -0
  588. package/dist/security/decision-event.d.ts +128 -0
  589. package/dist/security/decision-event.d.ts.map +1 -0
  590. package/dist/security/decision-event.js +225 -0
  591. package/dist/security/decision-event.js.map +1 -0
  592. package/dist/security/transport-decision.d.ts +6 -0
  593. package/dist/security/transport-decision.d.ts.map +1 -0
  594. package/dist/security/transport-decision.js +37 -0
  595. package/dist/security/transport-decision.js.map +1 -0
  596. package/dist/server.d.ts +50 -0
  597. package/dist/server.d.ts.map +1 -0
  598. package/dist/server.js +491 -0
  599. package/dist/server.js.map +1 -0
  600. package/dist/two-factor/cache.d.ts +47 -0
  601. package/dist/two-factor/cache.d.ts.map +1 -0
  602. package/dist/two-factor/cache.js +122 -0
  603. package/dist/two-factor/cache.js.map +1 -0
  604. package/dist/two-factor/crypto.d.ts +41 -0
  605. package/dist/two-factor/crypto.d.ts.map +1 -0
  606. package/dist/two-factor/crypto.js +94 -0
  607. package/dist/two-factor/crypto.js.map +1 -0
  608. package/dist/two-factor/errors.d.ts +82 -0
  609. package/dist/two-factor/errors.d.ts.map +1 -0
  610. package/dist/two-factor/errors.js +112 -0
  611. package/dist/two-factor/errors.js.map +1 -0
  612. package/dist/two-factor/index.d.ts +18 -0
  613. package/dist/two-factor/index.d.ts.map +1 -0
  614. package/dist/two-factor/index.js +25 -0
  615. package/dist/two-factor/index.js.map +1 -0
  616. package/dist/two-factor/otp.d.ts +43 -0
  617. package/dist/two-factor/otp.d.ts.map +1 -0
  618. package/dist/two-factor/otp.js +57 -0
  619. package/dist/two-factor/otp.js.map +1 -0
  620. package/dist/two-factor/recovery.d.ts +45 -0
  621. package/dist/two-factor/recovery.d.ts.map +1 -0
  622. package/dist/two-factor/recovery.js +104 -0
  623. package/dist/two-factor/recovery.js.map +1 -0
  624. package/dist/two-factor/repository.d.ts +156 -0
  625. package/dist/two-factor/repository.d.ts.map +1 -0
  626. package/dist/two-factor/repository.js +251 -0
  627. package/dist/two-factor/repository.js.map +1 -0
  628. package/dist/two-factor/service.d.ts +232 -0
  629. package/dist/two-factor/service.d.ts.map +1 -0
  630. package/dist/two-factor/service.js +599 -0
  631. package/dist/two-factor/service.js.map +1 -0
  632. package/dist/two-factor/totp.d.ts +57 -0
  633. package/dist/two-factor/totp.d.ts.map +1 -0
  634. package/dist/two-factor/totp.js +96 -0
  635. package/dist/two-factor/totp.js.map +1 -0
  636. package/dist/two-factor/types.d.ts +152 -0
  637. package/dist/two-factor/types.d.ts.map +1 -0
  638. package/dist/two-factor/types.js +64 -0
  639. package/dist/two-factor/types.js.map +1 -0
  640. package/dist/users/cache.d.ts +49 -0
  641. package/dist/users/cache.d.ts.map +1 -0
  642. package/dist/users/cache.js +140 -0
  643. package/dist/users/cache.js.map +1 -0
  644. package/dist/users/claims.d.ts +67 -0
  645. package/dist/users/claims.d.ts.map +1 -0
  646. package/dist/users/claims.js +104 -0
  647. package/dist/users/claims.js.map +1 -0
  648. package/dist/users/errors.d.ts +25 -0
  649. package/dist/users/errors.d.ts.map +1 -0
  650. package/dist/users/errors.js +31 -0
  651. package/dist/users/errors.js.map +1 -0
  652. package/dist/users/gdpr.d.ts +110 -0
  653. package/dist/users/gdpr.d.ts.map +1 -0
  654. package/dist/users/gdpr.js +211 -0
  655. package/dist/users/gdpr.js.map +1 -0
  656. package/dist/users/index.d.ts +20 -0
  657. package/dist/users/index.d.ts.map +1 -0
  658. package/dist/users/index.js +19 -0
  659. package/dist/users/index.js.map +1 -0
  660. package/dist/users/password.d.ts +85 -0
  661. package/dist/users/password.d.ts.map +1 -0
  662. package/dist/users/password.js +138 -0
  663. package/dist/users/password.js.map +1 -0
  664. package/dist/users/repository.d.ts +263 -0
  665. package/dist/users/repository.d.ts.map +1 -0
  666. package/dist/users/repository.js +499 -0
  667. package/dist/users/repository.js.map +1 -0
  668. package/dist/users/service.d.ts +299 -0
  669. package/dist/users/service.d.ts.map +1 -0
  670. package/dist/users/service.js +698 -0
  671. package/dist/users/service.js.map +1 -0
  672. package/dist/users/types.d.ts +220 -0
  673. package/dist/users/types.d.ts.map +1 -0
  674. package/dist/users/types.js +77 -0
  675. package/dist/users/types.js.map +1 -0
  676. package/locales/default/en/common.json +10 -0
  677. package/locales/default/en/consent.json +13 -0
  678. package/locales/default/en/emails.json +17 -0
  679. package/locales/default/en/errors.json +21 -0
  680. package/locales/default/en/forgot-password.json +11 -0
  681. package/locales/default/en/invitation.json +18 -0
  682. package/locales/default/en/login.json +17 -0
  683. package/locales/default/en/logout.json +9 -0
  684. package/locales/default/en/magic-link.json +12 -0
  685. package/locales/default/en/reset-password.json +16 -0
  686. package/locales/default/en/two-factor.json +29 -0
  687. package/migrations/.gitkeep +0 -0
  688. package/migrations/001_extensions.sql +22 -0
  689. package/migrations/002_organizations.sql +43 -0
  690. package/migrations/003_applications.sql +50 -0
  691. package/migrations/004_clients.sql +74 -0
  692. package/migrations/005_users.sql +119 -0
  693. package/migrations/006_roles_permissions.sql +80 -0
  694. package/migrations/007_custom_claims.sql +55 -0
  695. package/migrations/008_config.sql +51 -0
  696. package/migrations/009_audit_log.sql +31 -0
  697. package/migrations/010_oidc_adapter.sql +34 -0
  698. package/migrations/011_seed.sql +53 -0
  699. package/migrations/012_two_factor.sql +88 -0
  700. package/migrations/013_client_secret_sha256.sql +22 -0
  701. package/migrations/014_login_methods.sql +34 -0
  702. package/migrations/015_signing_key_encryption.sql +20 -0
  703. package/migrations/016_failed_login_count.sql +27 -0
  704. package/migrations/017_audit_retention.sql +11 -0
  705. package/migrations/018_admin_api_enhancements.sql +78 -0
  706. package/migrations/019_invitation_details.sql +27 -0
  707. package/migrations/020_two_factor_admin_permission.sql +108 -0
  708. package/migrations/021_auth_recovery_jobs.sql +74 -0
  709. package/migrations/022_recovery_artifact_ownership.sql +25 -0
  710. package/migrations/023_magic_link_authority.sql +39 -0
  711. package/package.json +106 -0
  712. package/templates/default/emails/invitation.hbs +39 -0
  713. package/templates/default/emails/invitation.txt.hbs +18 -0
  714. package/templates/default/emails/magic-link.hbs +30 -0
  715. package/templates/default/emails/magic-link.txt.hbs +11 -0
  716. package/templates/default/emails/otp-code.hbs +31 -0
  717. package/templates/default/emails/otp-code.txt.hbs +15 -0
  718. package/templates/default/emails/password-changed.hbs +24 -0
  719. package/templates/default/emails/password-changed.txt.hbs +7 -0
  720. package/templates/default/emails/password-reset.hbs +30 -0
  721. package/templates/default/emails/password-reset.txt.hbs +11 -0
  722. package/templates/default/emails/welcome.hbs +25 -0
  723. package/templates/default/emails/welcome.txt.hbs +9 -0
  724. package/templates/default/layouts/main.hbs +49 -0
  725. package/templates/default/pages/accept-invite.hbs +25 -0
  726. package/templates/default/pages/consent.hbs +27 -0
  727. package/templates/default/pages/error.hbs +15 -0
  728. package/templates/default/pages/forgot-password.hbs +21 -0
  729. package/templates/default/pages/invite-expired.hbs +6 -0
  730. package/templates/default/pages/invite-success.hbs +9 -0
  731. package/templates/default/pages/login.hbs +108 -0
  732. package/templates/default/pages/logout-success.hbs +5 -0
  733. package/templates/default/pages/logout.hbs +24 -0
  734. package/templates/default/pages/magic-link-sent.hbs +10 -0
  735. package/templates/default/pages/magic-link-success.hbs +17 -0
  736. package/templates/default/pages/reset-password.hbs +20 -0
  737. package/templates/default/pages/reset-success.hbs +9 -0
  738. package/templates/default/pages/two-factor-setup.hbs +65 -0
  739. package/templates/default/pages/two-factor-verify.hbs +77 -0
  740. package/templates/default/partials/flash-messages.hbs +10 -0
  741. package/templates/default/partials/footer.hbs +4 -0
  742. package/templates/default/partials/header.hbs +8 -0
@@ -0,0 +1,1074 @@
1
+ /**
2
+ * Interaction route handlers for the OIDC login, consent, and logout flows.
3
+ *
4
+ * These routes handle the `/interaction/:uid` path where the UID is a
5
+ * node-oidc-provider interaction identifier. The provider redirects users
6
+ * here during the OIDC authorization flow for login and consent prompts.
7
+ *
8
+ * Route structure:
9
+ * GET /interaction/:uid → showLogin or showConsent (based on prompt)
10
+ * POST /interaction/:uid/login → processLogin (password auth)
11
+ * POST /interaction/:uid/magic-link → sendMagicLink (passwordless auth)
12
+ * GET /interaction/:uid/consent → showConsent
13
+ * POST /interaction/:uid/confirm → processConsent (approve/deny)
14
+ * GET /interaction/:uid/abort → abortInteraction
15
+ *
16
+ * Security features:
17
+ * - CSRF protection on all POST endpoints
18
+ * - Rate limiting on login and magic link endpoints
19
+ * - User enumeration prevention (generic error messages)
20
+ * - Audit logging for all auth events
21
+ */
22
+ import Router from '@koa/router';
23
+ import { generateCsrfToken, verifyCsrfToken, setCsrfCookie, getCsrfFromCookie, } from '../auth/csrf.js';
24
+ import { checkRateLimit, resetRateLimit, buildLoginRateLimitKey, buildMagicLinkRateLimitKey, loadLoginRateLimitConfig, loadMagicLinkRateLimitConfig, } from '../auth/rate-limiter.js';
25
+ import { sendOtpCodeEmail } from '../auth/email-service.js';
26
+ import { enqueueAccountRecovery } from '../auth/recovery-service.js';
27
+ import { resolveLocale, getTranslationFunction } from '../auth/i18n.js';
28
+ import { renderPage } from '../auth/template-engine.js';
29
+ import { prepareUserForPasswordLogin, verifyLoginPassword, recordLogin, recordPasswordFailure, } from '../users/service.js';
30
+ import { requiresTwoFactor, determineTwoFactorMethod, sendOtpCode } from '../two-factor/service.js';
31
+ import { writeAuditLog } from '../lib/audit-log.js';
32
+ import { logger } from '../lib/logger.js';
33
+ import { getClientByClientId } from '../clients/service.js';
34
+ import { getOrganizationById } from '../organizations/service.js';
35
+ import { getRedis } from '../lib/redis.js';
36
+ import { hasMagicLinkSession, consumeMagicLinkSession } from '../auth/magic-link-session.js';
37
+ import { resolveLoginMethods } from '../clients/resolve-login-methods.js';
38
+ import { purgeExpired } from '../oidc/postgres-adapter.js';
39
+ import { observeProtocolSecurityRejection } from '../oidc/protocol-security-observer.js';
40
+ const defaultEnumerationDependencies = {
41
+ getCsrfFromCookie,
42
+ verifyCsrfToken,
43
+ generateCsrfToken,
44
+ setCsrfCookie,
45
+ buildLoginRateLimitKey,
46
+ buildMagicLinkRateLimitKey,
47
+ loadLoginRateLimitConfig,
48
+ loadMagicLinkRateLimitConfig,
49
+ checkRateLimit,
50
+ resetRateLimit,
51
+ resolveLocale,
52
+ getTranslationFunction,
53
+ renderPage,
54
+ prepareUserForPasswordLogin,
55
+ verifyLoginPassword,
56
+ recordPasswordFailure,
57
+ recordLogin,
58
+ enqueueAccountRecovery,
59
+ writeAuditLog,
60
+ };
61
+ // ---------------------------------------------------------------------------
62
+ // Helpers
63
+ // ---------------------------------------------------------------------------
64
+ /**
65
+ * Build a branding context object from organization data.
66
+ * Used to populate template context with org-specific branding.
67
+ *
68
+ * @param org - Organization with branding fields
69
+ * @returns Branding context for templates
70
+ */
71
+ /**
72
+ * Resolve the effective login methods for an OIDC provider Client.
73
+ *
74
+ * Reads the raw `urn:porta:login_methods` metadata (set by {@link findForOidc}
75
+ * on the clients service) and applies the inheritance rule via
76
+ * {@link resolveLoginMethods}:
77
+ * - `null` / missing / malformed → inherit `org.defaultLoginMethods`
78
+ * - explicit non-empty array → use it verbatim
79
+ *
80
+ * Malformed metadata (e.g., from a misconfigured adapter) is treated as
81
+ * `null` to match the safe-fallback semantics of the raw resolver — the
82
+ * organization default governs in that case rather than failing closed,
83
+ * which would break all logins if a single client's metadata were corrupt.
84
+ *
85
+ * @param client - OIDC provider Client instance (or null/undefined — safe fallback)
86
+ * @param org - Organization owning the client (read for `defaultLoginMethods`)
87
+ * @returns Resolved `LoginMethod[]` (always non-empty, matches resolver contract)
88
+ */
89
+ function resolveLoginMethodsFromOidcClient(client, org) {
90
+ // oidc-provider's Client objects expose metadata() → Record<string, unknown>.
91
+ // Missing client or missing metadata function → treat as fully inherited.
92
+ const metadata = client?.metadata?.();
93
+ const raw = metadata?.['urn:porta:login_methods'];
94
+ // Defensive: accept only null or string[]; treat anything else as null.
95
+ const normalized = Array.isArray(raw) ? raw : null;
96
+ return resolveLoginMethods(org, { loginMethods: normalized });
97
+ }
98
+ function buildBrandingFromOrg(org) {
99
+ return {
100
+ logoUrl: org.brandingLogoUrl,
101
+ faviconUrl: org.brandingFaviconUrl,
102
+ primaryColor: org.brandingPrimaryColor ?? '#3B82F6',
103
+ companyName: org.brandingCompanyName ?? org.name,
104
+ customCss: org.brandingCustomCss,
105
+ };
106
+ }
107
+ /**
108
+ * Build a base template context from an interaction context.
109
+ * Shared across all interaction route handlers to provide consistent
110
+ * branding, locale, and CSRF state.
111
+ *
112
+ * @param ctx - Koa context with organization state
113
+ * @param locale - Resolved locale string
114
+ * @param csrfToken - CSRF token for form protection
115
+ * @param orgSlug - Organization slug for template resolution
116
+ * @returns Base template context (without the `t` translation function)
117
+ */
118
+ function buildBaseContext(ctx, locale, csrfToken, orgSlug) {
119
+ return {
120
+ branding: buildBrandingFromOrg(ctx.state.organization),
121
+ locale,
122
+ csrfToken,
123
+ orgSlug,
124
+ };
125
+ }
126
+ /**
127
+ * Render an HTML page and send it as the response.
128
+ * Sets Content-Type to text/html and assigns the rendered body to ctx.body.
129
+ *
130
+ * @param ctx - Koa context
131
+ * @param pageName - Page template name
132
+ * @param context - Full template context
133
+ * @param statusCode - HTTP status code (default: 200)
134
+ */
135
+ async function renderAndRespond(ctx, pageName, context, statusCode = 200, renderer = renderPage) {
136
+ const html = await renderer(pageName, context);
137
+ ctx.status = statusCode;
138
+ ctx.type = 'text/html';
139
+ ctx.body = html;
140
+ }
141
+ /**
142
+ * Resolve the organization from the OIDC interaction's client_id.
143
+ *
144
+ * Interaction routes (`/interaction/:uid`) don't go through the tenant
145
+ * resolver middleware, so `ctx.state.organization` is not set automatically.
146
+ * This helper resolves the org by looking up the client → organization chain
147
+ * from the interaction's `client_id` parameter, and sets `ctx.state.organization`.
148
+ *
149
+ * Also used by two-factor routes which share the same `/interaction` prefix
150
+ * and likewise don't go through the tenant resolver middleware.
151
+ *
152
+ * @param ctx - Koa context to populate with organization
153
+ * @param clientId - OIDC client_id from the interaction params
154
+ * @throws Error if client or organization cannot be found
155
+ */
156
+ export async function resolveOrganizationForInteraction(ctx, clientId, interactionUid) {
157
+ // If already resolved (e.g., by tenant resolver), skip
158
+ if (ctx.state.organization)
159
+ return;
160
+ // Prefer the auth-flow org stored in Redis during interaction creation.
161
+ // OIDC entry middleware has already proved that this organization owns the client.
162
+ if (interactionUid) {
163
+ try {
164
+ const storedOrgId = await getRedis().get(`interaction:org:${interactionUid}`);
165
+ if (storedOrgId) {
166
+ const org = await getOrganizationById(storedOrgId);
167
+ if (org) {
168
+ ctx.state.organization = org;
169
+ return;
170
+ }
171
+ }
172
+ }
173
+ catch {
174
+ // Redis error — fall through to client-based resolution
175
+ }
176
+ }
177
+ // Fallback: resolve from the client's organizationId
178
+ const client = await getClientByClientId(clientId);
179
+ if (!client) {
180
+ throw new Error(`Client not found for interaction: ${clientId}`);
181
+ }
182
+ const org = await getOrganizationById(client.organizationId);
183
+ if (!org) {
184
+ throw new Error(`Organization not found for client: ${client.organizationId}`);
185
+ }
186
+ ctx.state.organization = org;
187
+ }
188
+ /**
189
+ * Resolve continuation authority independently from the magic-link session value.
190
+ *
191
+ * The provider writes this tenant mapping when it creates the interaction. A missing, expired, or
192
+ * malformed mapping cannot authorize consumption and leaves the short-lived session untouched.
193
+ *
194
+ * @param interactionUid - Exact interaction selected by the public route.
195
+ * @returns Active interaction authority, or null when it cannot be proven.
196
+ */
197
+ async function resolveMagicLinkContinuationAuthority(interactionUid) {
198
+ if (interactionUid.length < 1 || interactionUid.length > 128)
199
+ return null;
200
+ const organizationId = await getRedis().get(`interaction:org:${interactionUid}`);
201
+ if (!organizationId)
202
+ return null;
203
+ const organization = await getOrganizationById(organizationId);
204
+ if (!organization || organization.status !== 'active')
205
+ return null;
206
+ return { authority: { organizationId, interactionUid }, organization };
207
+ }
208
+ /**
209
+ * Derive recovery-job authority from the provider interaction and its persisted client owner.
210
+ *
211
+ * @param routeInteractionUid - Interaction selected by the route.
212
+ * @param interactionUid - Interaction identifier returned by oidc-provider.
213
+ * @param clientIdValue - Client identifier returned by oidc-provider.
214
+ * @param clientOrganizationIdValue - Tenant metadata returned by the admitted provider client.
215
+ * @param organization - Tenant resolved for the interaction.
216
+ * @returns Exact job authority, or null when route, provider, client, and tenant do not agree.
217
+ */
218
+ function deriveMagicLinkRecoveryAuthority(routeInteractionUid, interactionUid, clientIdValue, clientOrganizationIdValue, organization) {
219
+ if (routeInteractionUid !== interactionUid ||
220
+ interactionUid.length < 1 ||
221
+ interactionUid.length > 128 ||
222
+ typeof clientIdValue !== 'string' ||
223
+ clientIdValue.length < 1 ||
224
+ clientOrganizationIdValue !== organization.id) {
225
+ return null;
226
+ }
227
+ return { organizationId: organization.id, interactionUid };
228
+ }
229
+ // ---------------------------------------------------------------------------
230
+ // Router factory
231
+ // ---------------------------------------------------------------------------
232
+ /**
233
+ * Create the interaction router with all OIDC interaction handlers.
234
+ *
235
+ * The router handles login, consent, magic link, and abort flows.
236
+ * Requires a Provider instance to interact with the OIDC session state.
237
+ *
238
+ * @param provider - node-oidc-provider instance for interaction management
239
+ * @returns Koa router with interaction routes mounted at /interaction
240
+ */
241
+ export function createInteractionRouter(provider, enumerationDependencies = defaultEnumerationDependencies) {
242
+ const router = new Router({ prefix: '/interaction' });
243
+ router.use(async (ctx, next) => {
244
+ try {
245
+ await next();
246
+ }
247
+ catch (error) {
248
+ if (isMissingInteraction(error)) {
249
+ observeProtocolSecurityRejection({
250
+ request: ctx.req,
251
+ eventClass: 'interaction-context-rejected',
252
+ });
253
+ }
254
+ throw error;
255
+ }
256
+ });
257
+ // -------------------------------------------------------------------------
258
+ // GET /interaction/:uid — Show login or consent page
259
+ // -------------------------------------------------------------------------
260
+ router.get('/:uid', async (ctx) => {
261
+ await showLogin(ctx, provider);
262
+ });
263
+ // -------------------------------------------------------------------------
264
+ // POST /interaction/:uid/login — Process password login
265
+ // -------------------------------------------------------------------------
266
+ router.post('/:uid/login', async (ctx) => {
267
+ await processLogin(ctx, provider, enumerationDependencies);
268
+ });
269
+ // -------------------------------------------------------------------------
270
+ // POST /interaction/:uid/magic-link — Send magic link email
271
+ // -------------------------------------------------------------------------
272
+ router.post('/:uid/magic-link', async (ctx) => {
273
+ await handleSendMagicLink(ctx, provider, enumerationDependencies);
274
+ });
275
+ // -------------------------------------------------------------------------
276
+ // GET /interaction/:uid/consent — Show consent page
277
+ // -------------------------------------------------------------------------
278
+ router.get('/:uid/consent', async (ctx) => {
279
+ await showConsent(ctx, provider);
280
+ });
281
+ // -------------------------------------------------------------------------
282
+ // POST /interaction/:uid/confirm — Process consent (approve/deny)
283
+ // -------------------------------------------------------------------------
284
+ router.post('/:uid/confirm', async (ctx) => {
285
+ await processConsent(ctx, provider);
286
+ });
287
+ // -------------------------------------------------------------------------
288
+ // GET /interaction/:uid/abort — Abort the interaction
289
+ // -------------------------------------------------------------------------
290
+ router.get('/:uid/abort', async (ctx) => {
291
+ await abortInteraction(ctx, provider);
292
+ });
293
+ return router;
294
+ }
295
+ /** Identifies the provider's exact expired, absent, or mismatched interaction state error. */
296
+ function isMissingInteraction(error) {
297
+ if (typeof error !== 'object' || error === null || Array.isArray(error))
298
+ return false;
299
+ return Reflect.get(error, 'name') === 'SessionNotFound' && Reflect.get(error, 'status') === 400;
300
+ }
301
+ // ---------------------------------------------------------------------------
302
+ // Route handlers
303
+ // ---------------------------------------------------------------------------
304
+ /**
305
+ * Show the login page for an OIDC interaction.
306
+ *
307
+ * Checks for a `_ml_session` cookie first (set by the magic link handler).
308
+ * If present, consumes the session and either:
309
+ * - Completes the OIDC flow via interactionFinished() (same browser)
310
+ * - Shows a "You're signed in" success page (different browser)
311
+ *
312
+ * If no magic link session, falls through to the normal login flow:
313
+ * 1. Loads the interaction details from the provider
314
+ * 2. If the prompt is "consent", redirects to the consent page
315
+ * 3. Resolves the locale from OIDC params, Accept-Language, and org default
316
+ * 4. Generates a CSRF token for form protection
317
+ * 5. Renders the login page with branding, i18n, and interaction data
318
+ *
319
+ * @param ctx - Koa context with organization state
320
+ * @param provider - OIDC provider instance
321
+ */
322
+ async function showLogin(ctx, provider) {
323
+ // -----------------------------------------------------------------------
324
+ // Opportunistic cleanup: purge expired oidc_payloads records.
325
+ // Fire-and-forget — no await, no error propagation. Keeps the database
326
+ // clean without a cron job. Proven pattern from Porta v4.
327
+ // -----------------------------------------------------------------------
328
+ purgeExpired();
329
+ // -----------------------------------------------------------------------
330
+ // Magic link session detection
331
+ // -----------------------------------------------------------------------
332
+ if (hasMagicLinkSession(ctx)) {
333
+ const continuationAuthority = await resolveMagicLinkContinuationAuthority(ctx.params.uid);
334
+ const session = continuationAuthority
335
+ ? await consumeMagicLinkSession(ctx, continuationAuthority.authority)
336
+ : null;
337
+ if (session && continuationAuthority) {
338
+ const org = continuationAuthority.organization;
339
+ // Try to complete the OIDC flow (same browser — interaction cookies present)
340
+ try {
341
+ const result = {
342
+ login: { accountId: session.userId },
343
+ };
344
+ await provider.interactionFinished(ctx.req, ctx.res, result, {
345
+ mergeWithLastSubmission: false,
346
+ });
347
+ return; // Success! Browser redirected to callback with code
348
+ }
349
+ catch {
350
+ // Different browser — interaction cookies not present, or interaction expired.
351
+ // Show the magic link success page instead.
352
+ logger.info({ event: 'magic_link_cross_browser_success' }, 'Magic link interaction cookies unavailable; showing success page');
353
+ await renderMagicLinkSuccessPage(ctx, org);
354
+ return;
355
+ }
356
+ }
357
+ // Invalid/expired session — fall through to normal login
358
+ }
359
+ // -----------------------------------------------------------------------
360
+ // Normal login flow
361
+ // -----------------------------------------------------------------------
362
+ try {
363
+ const interaction = await provider.interactionDetails(ctx.req, ctx.res);
364
+ const { prompt, params } = interaction;
365
+ // Resolve organization from the interaction's client_id.
366
+ // Interaction routes don't go through the tenant resolver middleware,
367
+ // so we resolve the org from the client → organization chain.
368
+ await resolveOrganizationForInteraction(ctx, params.client_id, interaction.uid);
369
+ // If the prompt is consent, handle it directly (no redirect).
370
+ // Redirecting to /consent as a separate request can lose the interaction
371
+ // cookie in some flows (e.g., pre-auth magic link), because the cookie
372
+ // was set by the provider on the redirect chain and a Koa redirect
373
+ // doesn't carry it forward reliably.
374
+ if (prompt.name === 'consent') {
375
+ await showConsent(ctx, provider);
376
+ return;
377
+ }
378
+ const org = ctx.state.organization;
379
+ // Resolve locale from OIDC ui_locales, Accept-Language, and org default
380
+ const uiLocales = params.ui_locales;
381
+ const acceptLanguage = ctx.get('Accept-Language') || undefined;
382
+ const locale = await resolveLocale(uiLocales, acceptLanguage, org.defaultLocale);
383
+ // Get translation function with org-specific overrides
384
+ const t = getTranslationFunction(locale, org.slug);
385
+ // Generate CSRF token for form protection
386
+ const csrfToken = generateCsrfToken();
387
+ setCsrfCookie(ctx, csrfToken);
388
+ // Pre-fill email from login_hint if provided.
389
+ //
390
+ // login_hint comes from the client-supplied authorization URL — OIDC spec
391
+ // allows any opaque identifier, not just emails, so we do NOT validate
392
+ // format. We DO defensively trim and length-cap at RFC 5321 max (320 chars)
393
+ // to avoid pathological template renders. Handlebars HTML-escapes on
394
+ // interpolation, which blocks XSS via the value="" attribute.
395
+ const rawHint = typeof params.login_hint === 'string' ? params.login_hint.trim().slice(0, 320) : '';
396
+ const emailHint = rawHint.length > 0 ? rawHint : undefined;
397
+ // Resolve human-readable client name from OIDC provider metadata.
398
+ // Falls back to the raw client_id if not found.
399
+ const oidcClient = await provider.Client.find(params.client_id);
400
+ const clientName = oidcClient?.metadata()?.client_name ?? params.client_id;
401
+ // Resolve the effective login methods from the client's OIDC metadata.
402
+ // When no client is found (edge case — client deleted mid-flow), fall back
403
+ // to the org default methods so the user sees SOMETHING actionable instead
404
+ // of a blank page.
405
+ const effectiveMethods = oidcClient
406
+ ? resolveLoginMethodsFromOidcClient(oidcClient, org)
407
+ : org.defaultLoginMethods;
408
+ const showPassword = effectiveMethods.includes('password');
409
+ const showMagicLink = effectiveMethods.includes('magic_link');
410
+ const context = {
411
+ ...buildBaseContext(ctx, locale, csrfToken, org.slug),
412
+ t,
413
+ interaction: {
414
+ uid: interaction.uid,
415
+ prompt: prompt.name,
416
+ params: params,
417
+ client: { clientName },
418
+ },
419
+ email: emailHint ?? '',
420
+ emailHint,
421
+ // Login method rendering flags — consumed by login.hbs
422
+ showPassword,
423
+ showMagicLink,
424
+ showDivider: showPassword && showMagicLink,
425
+ loginMethods: effectiveMethods,
426
+ };
427
+ // Render the login page with CSRF token in both cookie and hidden field
428
+ await renderAndRespond(ctx, 'login', context);
429
+ }
430
+ catch {
431
+ logger.error({ event: 'login-page-failed' }, 'Login page rendering failed');
432
+ await renderErrorPage(ctx, 'errors.interaction_expired');
433
+ }
434
+ }
435
+ /**
436
+ * Render the magic link success page — "You're signed in".
437
+ *
438
+ * Shown when a magic link is opened in a different browser than the one
439
+ * that started the OIDC authorization flow. The OIDC flow can't be
440
+ * completed (no interaction cookies), but the user IS authenticated.
441
+ *
442
+ * Security guard: this function is ONLY called when a valid `_ml_session`
443
+ * was present and successfully consumed. The `_ml_session` is single-use
444
+ * (Redis key deleted), so this page cannot be shown twice or by URL crafting.
445
+ *
446
+ * @param ctx - Koa context
447
+ * @param org - Organization for branding
448
+ */
449
+ async function renderMagicLinkSuccessPage(ctx, org) {
450
+ const locale = await resolveLocale(undefined, ctx.get('Accept-Language') || undefined, org.defaultLocale);
451
+ const t = getTranslationFunction(locale, org.slug);
452
+ const csrfToken = generateCsrfToken();
453
+ const context = {
454
+ branding: buildBrandingFromOrg(org),
455
+ locale,
456
+ t,
457
+ csrfToken,
458
+ orgSlug: org.slug,
459
+ };
460
+ await renderAndRespond(ctx, 'magic-link-success', context);
461
+ }
462
+ /**
463
+ * Process a password-based login attempt.
464
+ *
465
+ * 1. Verifies CSRF token from form submission
466
+ * 2. Extracts email + password from the request body
467
+ * 3. Checks rate limits (per IP+email combination)
468
+ * 4. Looks up user by org + email
469
+ * 5. Verifies password and checks user status
470
+ * 6. On success: records login, resets rate limit, finishes interaction
471
+ * 7. On failure: renders login page with generic error (prevents enumeration)
472
+ *
473
+ * @param ctx - Koa context with organization state
474
+ * @param provider - OIDC provider instance
475
+ */
476
+ async function processLogin(ctx, provider, dependencies) {
477
+ const body = ctx.request.body;
478
+ const email = (body.email ?? '').trim().toLowerCase();
479
+ const password = body.password ?? '';
480
+ const submittedCsrf = body._csrf ?? '';
481
+ const storedCsrf = dependencies.getCsrfFromCookie(ctx) ?? '';
482
+ try {
483
+ const interaction = await provider.interactionDetails(ctx.req, ctx.res);
484
+ // Resolve organization from the interaction's client_id
485
+ await resolveOrganizationForInteraction(ctx, interaction.params.client_id, interaction.uid);
486
+ const org = ctx.state.organization;
487
+ // Resolve locale for error messages
488
+ const locale = await dependencies.resolveLocale(undefined, ctx.get('Accept-Language') || undefined, org.defaultLocale);
489
+ const t = dependencies.getTranslationFunction(locale, org.slug);
490
+ // Step 1: Verify CSRF token (cookie vs form field)
491
+ if (!dependencies.verifyCsrfToken(storedCsrf, submittedCsrf)) {
492
+ logger.warn({ event: 'login-csrf-rejected' }, 'Login request CSRF validation failed');
493
+ await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.csrf_invalid'), 200, dependencies);
494
+ return;
495
+ }
496
+ // Step 1.5: Enforce login method — 'password' must be allowed for this client.
497
+ //
498
+ // This check protects against three scenarios:
499
+ // 1. User submits the password form after an admin switched the client
500
+ // to magic-link-only (stale form cached in the browser).
501
+ // 2. Adversary crafts a direct POST to bypass the UI guard (the template
502
+ // hides the password form, but a raw POST from curl/Postman still
503
+ // reaches this handler).
504
+ // 3. Browser back button resurrects the form after login method change.
505
+ //
506
+ // Rendering the login page (not a raw 403) preserves CSRF/state and gives
507
+ // the user a clear, actionable error message.
508
+ const oidcClientForLogin = await provider.Client.find(interaction.params.client_id);
509
+ const effectiveLoginMethods = oidcClientForLogin
510
+ ? resolveLoginMethodsFromOidcClient(oidcClientForLogin, org)
511
+ : org.defaultLoginMethods;
512
+ if (!effectiveLoginMethods.includes('password')) {
513
+ logger.warn({ event: 'password-login-method-disabled' }, 'Password login method is disabled');
514
+ dependencies.writeAuditLog({
515
+ organizationId: org.id,
516
+ eventType: 'security.login_method_disabled',
517
+ eventCategory: 'security',
518
+ description: 'Password login attempted where the method is disabled',
519
+ ipAddress: ctx.ip,
520
+ metadata: {
521
+ clientId: interaction.params.client_id,
522
+ attemptedMethod: 'password',
523
+ effectiveMethods: effectiveLoginMethods,
524
+ },
525
+ });
526
+ await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.login_method_disabled'), 403, dependencies);
527
+ return;
528
+ }
529
+ // Step 2: Check rate limit
530
+ const rateLimitKey = dependencies.buildLoginRateLimitKey(org.id, ctx.ip, email);
531
+ const rateLimitConfig = await dependencies.loadLoginRateLimitConfig();
532
+ const rateLimitResult = await dependencies.checkRateLimit(rateLimitKey, rateLimitConfig);
533
+ if (!rateLimitResult.allowed) {
534
+ ctx.set('Retry-After', String(rateLimitResult.retryAfter));
535
+ // Audit: rate limit exceeded
536
+ dependencies.writeAuditLog({
537
+ organizationId: org.id,
538
+ eventType: 'rate_limit.login',
539
+ eventCategory: 'security',
540
+ description: 'Login rate limit exceeded',
541
+ ipAddress: ctx.ip,
542
+ });
543
+ await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.rate_limit_exceeded'), 429, dependencies);
544
+ return;
545
+ }
546
+ // Resolve cooldown state and verify exactly one real or dummy Argon2id hash. Only active
547
+ // accounts with passwords receive authentication authority; every other state follows the
548
+ // same verification and persistence shape.
549
+ const observedUser = await dependencies.prepareUserForPasswordLogin(org.id, email);
550
+ const eligibleUser = observedUser?.status === 'active' && observedUser.hasPassword ? observedUser : null;
551
+ const passwordValid = await dependencies.verifyLoginPassword(eligibleUser?.id ?? null, password);
552
+ if (!eligibleUser || !passwordValid) {
553
+ const lockResult = await dependencies.recordPasswordFailure(eligibleUser);
554
+ void dependencies.writeAuditLog({
555
+ organizationId: org.id,
556
+ eventType: 'user.login.password.failed',
557
+ eventCategory: 'security',
558
+ description: 'Password login rejected',
559
+ ipAddress: ctx.ip,
560
+ metadata: { failedCount: lockResult.failedCount, autoLocked: lockResult.locked },
561
+ });
562
+ await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('login.error_invalid'), 200, dependencies);
563
+ return;
564
+ }
565
+ const user = eligibleUser;
566
+ // Step 6: Password valid — check if 2FA is required
567
+ await dependencies.resetRateLimit(rateLimitKey);
568
+ // Determine 2FA requirements based on org policy and user state
569
+ const twoFactorRequired = user.twoFactorEnabled || requiresTwoFactor(org, user);
570
+ const twoFactorMethod = determineTwoFactorMethod(org, user);
571
+ if (twoFactorRequired) {
572
+ // Store pending login in the interaction session — user must complete 2FA
573
+ await provider.interactionResult(ctx.req, ctx.res, {
574
+ twoFactor: {
575
+ pendingAccountId: user.id,
576
+ method: twoFactorMethod ?? 'email',
577
+ email: user.email,
578
+ },
579
+ }, { mergeWithLastSubmission: true });
580
+ // If method is email, auto-send the first OTP code
581
+ if (twoFactorMethod === 'email' || (!user.twoFactorEnabled && !twoFactorMethod)) {
582
+ try {
583
+ const otpCode = await sendOtpCode(user.id, user.email, org.id);
584
+ // Send the OTP code via email (fire-and-forget)
585
+ sendOtpCodeEmail({
586
+ id: user.id,
587
+ email: user.email,
588
+ givenName: user.givenName,
589
+ familyName: user.familyName,
590
+ }, {
591
+ id: org.id,
592
+ slug: org.slug,
593
+ brandingLogoUrl: org.brandingLogoUrl,
594
+ brandingPrimaryColor: org.brandingPrimaryColor,
595
+ brandingCompanyName: org.brandingCompanyName,
596
+ }, otpCode, 10, locale);
597
+ }
598
+ catch {
599
+ logger.warn({ event: 'login-otp-delivery-failed' }, 'Initial OTP delivery failed');
600
+ }
601
+ }
602
+ writeAuditLog({
603
+ organizationId: org.id,
604
+ userId: user.id,
605
+ eventType: '2fa.challenge.started',
606
+ eventCategory: 'authentication',
607
+ description: `Two-factor challenge initiated (${twoFactorMethod ?? 'setup_required'})`,
608
+ ipAddress: ctx.ip,
609
+ });
610
+ // Redirect to the 2FA verification page (or setup page if not enrolled)
611
+ if (user.twoFactorEnabled) {
612
+ ctx.redirect(`/interaction/${interaction.uid}/two-factor`);
613
+ }
614
+ else {
615
+ // User needs to set up 2FA first (org requires it but user hasn't enrolled)
616
+ ctx.redirect(`/interaction/${interaction.uid}/two-factor/setup`);
617
+ }
618
+ return;
619
+ }
620
+ // No 2FA required — complete login normally
621
+ await dependencies.recordLogin(user.id);
622
+ // Audit: successful login (no 2FA)
623
+ dependencies.writeAuditLog({
624
+ organizationId: org.id,
625
+ userId: user.id,
626
+ eventType: 'user.login.password',
627
+ eventCategory: 'authentication',
628
+ description: 'Password login successful',
629
+ ipAddress: ctx.ip,
630
+ });
631
+ // Finish the OIDC interaction with the logged-in user
632
+ const result = {
633
+ login: { accountId: user.id },
634
+ };
635
+ await provider.interactionFinished(ctx.req, ctx.res, result, {
636
+ mergeWithLastSubmission: false,
637
+ });
638
+ }
639
+ catch {
640
+ logger.error({ event: 'login-processing-failed' }, 'Login processing failed');
641
+ await renderErrorPage(ctx, 'errors.interaction_expired');
642
+ }
643
+ }
644
+ /**
645
+ * Send a magic link email for passwordless authentication.
646
+ *
647
+ * Always shows the "check your email" page regardless of whether the
648
+ * user exists — this prevents user enumeration attacks. If the user is
649
+ * found and active, generates a token, stores it, and sends the email.
650
+ *
651
+ * @param ctx - Koa context with organization state
652
+ * @param provider - OIDC provider instance
653
+ */
654
+ async function handleSendMagicLink(ctx, provider, dependencies) {
655
+ const body = ctx.request.body;
656
+ const email = (body.email ?? '').trim().toLowerCase();
657
+ const submittedCsrf = body._csrf ?? '';
658
+ const storedCsrf = dependencies.getCsrfFromCookie(ctx) ?? '';
659
+ try {
660
+ const interaction = await provider.interactionDetails(ctx.req, ctx.res);
661
+ // Resolve organization from the interaction's client_id
662
+ await resolveOrganizationForInteraction(ctx, interaction.params.client_id, interaction.uid);
663
+ const org = ctx.state.organization;
664
+ // Resolve locale for page rendering
665
+ const locale = await dependencies.resolveLocale(undefined, ctx.get('Accept-Language') || undefined, org.defaultLocale);
666
+ const t = dependencies.getTranslationFunction(locale, org.slug);
667
+ // Verify CSRF token (cookie vs form field)
668
+ if (!dependencies.verifyCsrfToken(storedCsrf, submittedCsrf)) {
669
+ logger.warn({ event: 'magic_link_csrf_rejected' }, 'Magic link request rejected by CSRF validation');
670
+ await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.csrf_invalid'), 200, dependencies);
671
+ return;
672
+ }
673
+ // Enforce login method — 'magic_link' must be allowed for this client.
674
+ // Mirror of the password-login enforcement above. Same rationale: backend
675
+ // is the authoritative guard; the template hiding the button is cosmetic.
676
+ const oidcClientForMagicLink = await provider.Client.find(interaction.params.client_id);
677
+ const effectiveMagicLinkMethods = oidcClientForMagicLink
678
+ ? resolveLoginMethodsFromOidcClient(oidcClientForMagicLink, org)
679
+ : org.defaultLoginMethods;
680
+ if (!effectiveMagicLinkMethods.includes('magic_link')) {
681
+ logger.warn({
682
+ event: 'magic_link_method_disabled',
683
+ organizationId: org.id,
684
+ clientId: interaction.params.client_id,
685
+ }, 'Magic link attempted for client where method is disabled');
686
+ dependencies.writeAuditLog({
687
+ organizationId: org.id,
688
+ eventType: 'security.login_method_disabled',
689
+ eventCategory: 'security',
690
+ description: 'Magic link attempted on client where method is disabled',
691
+ ipAddress: ctx.ip,
692
+ metadata: {
693
+ clientId: interaction.params.client_id,
694
+ attemptedMethod: 'magic_link',
695
+ effectiveMethods: effectiveMagicLinkMethods,
696
+ },
697
+ });
698
+ await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.login_method_disabled'), 403, dependencies);
699
+ return;
700
+ }
701
+ const recoveryAuthority = deriveMagicLinkRecoveryAuthority(ctx.params.uid, interaction.uid, interaction.params.client_id, oidcClientForMagicLink?.metadata().organizationId, org);
702
+ if (!recoveryAuthority) {
703
+ logger.warn({ event: 'magic_link_authority_rejected' }, 'Magic link authority rejected');
704
+ await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.interaction_expired'), 400, dependencies);
705
+ return;
706
+ }
707
+ // Check rate limit for magic link requests
708
+ const rateLimitKey = dependencies.buildMagicLinkRateLimitKey(org.id, email);
709
+ const rateLimitConfig = await dependencies.loadMagicLinkRateLimitConfig();
710
+ const rateLimitResult = await dependencies.checkRateLimit(rateLimitKey, rateLimitConfig);
711
+ if (!rateLimitResult.allowed) {
712
+ ctx.set('Retry-After', String(rateLimitResult.retryAfter));
713
+ dependencies.writeAuditLog({
714
+ organizationId: org.id,
715
+ eventType: 'rate_limit.magic_link',
716
+ eventCategory: 'security',
717
+ description: 'Magic link rate limit exceeded',
718
+ ipAddress: ctx.ip,
719
+ });
720
+ await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.rate_limit_exceeded'), 429, dependencies);
721
+ return;
722
+ }
723
+ try {
724
+ await dependencies.enqueueAccountRecovery({
725
+ jobType: 'magic_link',
726
+ organizationId: recoveryAuthority.organizationId,
727
+ email,
728
+ interactionUid: recoveryAuthority.interactionUid,
729
+ actionNonce: submittedCsrf,
730
+ });
731
+ }
732
+ catch {
733
+ logger.error({ event: 'magic_link_enqueue_failed' }, 'Failed to enqueue magic link');
734
+ await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.interaction_expired'), 503, dependencies);
735
+ return;
736
+ }
737
+ // Always render the "check your email" page — prevents user enumeration
738
+ const csrfToken = dependencies.generateCsrfToken();
739
+ const context = {
740
+ ...buildBaseContext(ctx, locale, csrfToken, org.slug),
741
+ t,
742
+ email,
743
+ loginUrl: `/interaction/${interaction.uid}`,
744
+ };
745
+ await renderAndRespond(ctx, 'magic-link-sent', context, 200, dependencies.renderPage);
746
+ }
747
+ catch {
748
+ logger.error({ event: 'magic_link_request_failed' }, 'Magic link request failed');
749
+ await renderErrorPage(ctx, 'errors.interaction_expired');
750
+ }
751
+ }
752
+ /**
753
+ * Show the consent page for an OIDC interaction.
754
+ *
755
+ * If the client belongs to the same organization (first-party app),
756
+ * auto-consents by immediately finishing the interaction. Otherwise,
757
+ * renders the consent page with requested scopes and client info.
758
+ *
759
+ * @param ctx - Koa context with organization state
760
+ * @param provider - OIDC provider instance
761
+ */
762
+ async function showConsent(ctx, provider) {
763
+ try {
764
+ const interaction = await provider.interactionDetails(ctx.req, ctx.res);
765
+ const { prompt, params } = interaction;
766
+ // Resolve organization from the interaction's client_id
767
+ await resolveOrganizationForInteraction(ctx, params.client_id, interaction.uid);
768
+ const org = ctx.state.organization;
769
+ // Resolve locale for the consent page
770
+ const uiLocales = params.ui_locales;
771
+ const acceptLanguage = ctx.get('Accept-Language') || undefined;
772
+ const locale = await resolveLocale(uiLocales, acceptLanguage, org.defaultLocale);
773
+ const t = getTranslationFunction(locale, org.slug);
774
+ // Auto-consent for first-party apps: if the client metadata includes
775
+ // the org ID matching the current tenant, skip the consent screen
776
+ const clientId = params.client_id;
777
+ const client = await provider.Client.find(clientId);
778
+ if (client) {
779
+ // Check if this is a first-party client (belongs to the same org)
780
+ const clientOrgId = client.metadata()?.organizationId;
781
+ if (clientOrgId && clientOrgId === org.id) {
782
+ // Auto-consent: finish interaction immediately with consent grant
783
+ const grant = new provider.Grant({ accountId: interaction.session?.accountId, clientId });
784
+ // Use prompt.details to grant exactly what the provider requires
785
+ const details = prompt.details;
786
+ if (details.missingOIDCScope) {
787
+ const scopes = details.missingOIDCScope;
788
+ grant.addOIDCScope([...scopes].join(' '));
789
+ }
790
+ if (details.missingOIDCClaims) {
791
+ const claims = details.missingOIDCClaims;
792
+ grant.addOIDCClaims([...claims]);
793
+ }
794
+ if (details.missingResourceScopes) {
795
+ for (const [indicator, scopes] of Object.entries(details.missingResourceScopes)) {
796
+ grant.addResourceScope(indicator, [...scopes].join(' '));
797
+ }
798
+ }
799
+ // Explicitly grant offline_access if requested — node-oidc-provider
800
+ // treats it as a consent signal and may not include it in missingOIDCScope,
801
+ // but the Grant must include it for a refresh_token to be issued.
802
+ const requestedScope = params.scope ?? '';
803
+ if (requestedScope.includes('offline_access')) {
804
+ grant.addOIDCScope('offline_access');
805
+ }
806
+ const grantId = await grant.save();
807
+ const result = {
808
+ consent: { grantId },
809
+ };
810
+ writeAuditLog({
811
+ organizationId: org.id,
812
+ userId: interaction.session?.accountId,
813
+ eventType: 'user.consent.granted',
814
+ eventCategory: 'authentication',
815
+ description: `Auto-consent granted for first-party client ${clientId}`,
816
+ });
817
+ await provider.interactionFinished(ctx.req, ctx.res, result, {
818
+ mergeWithLastSubmission: true,
819
+ });
820
+ return;
821
+ }
822
+ }
823
+ // Third-party client: show the consent page
824
+ const csrfToken = generateCsrfToken();
825
+ setCsrfCookie(ctx, csrfToken);
826
+ const requestedScopes = (params.scope ?? '').split(' ').filter(Boolean);
827
+ const context = {
828
+ ...buildBaseContext(ctx, locale, csrfToken, org.slug),
829
+ t,
830
+ interaction: {
831
+ uid: interaction.uid,
832
+ prompt: prompt.name,
833
+ params: params,
834
+ client: { clientName: client?.metadata()?.client_name ?? clientId },
835
+ },
836
+ scopes: requestedScopes,
837
+ clientName: client?.metadata()?.client_name ?? clientId,
838
+ };
839
+ await renderAndRespond(ctx, 'consent', context);
840
+ }
841
+ catch {
842
+ logger.error({ event: 'consent-page-failed' }, 'Consent page rendering failed');
843
+ await renderErrorPage(ctx, 'errors.interaction_expired');
844
+ }
845
+ }
846
+ /**
847
+ * Process a consent decision (approve or deny).
848
+ *
849
+ * Reads the decision from the form body. On approval, creates a Grant
850
+ * with the requested scopes. On denial, finishes the interaction with
851
+ * an access_denied error.
852
+ *
853
+ * @param ctx - Koa context with organization state
854
+ * @param provider - OIDC provider instance
855
+ */
856
+ async function processConsent(ctx, provider) {
857
+ const body = ctx.request.body;
858
+ const submittedCsrf = body._csrf ?? '';
859
+ const storedCsrf = getCsrfFromCookie(ctx) ?? '';
860
+ const decision = body.decision; // 'approve' or 'deny'
861
+ try {
862
+ const interaction = await provider.interactionDetails(ctx.req, ctx.res);
863
+ // Resolve organization from the interaction's client_id
864
+ await resolveOrganizationForInteraction(ctx, interaction.params.client_id, interaction.uid);
865
+ const org = ctx.state.organization;
866
+ // Verify CSRF token
867
+ if (!verifyCsrfToken(storedCsrf, submittedCsrf)) {
868
+ logger.warn({ event: 'consent-csrf-rejected' }, 'Consent request CSRF validation failed');
869
+ ctx.status = 403;
870
+ ctx.body = 'Invalid CSRF token';
871
+ return;
872
+ }
873
+ if (decision === 'approve') {
874
+ // Create a grant for the approved scopes and claims
875
+ const clientId = interaction.params.client_id;
876
+ const grant = new provider.Grant({
877
+ accountId: interaction.session?.accountId,
878
+ clientId,
879
+ });
880
+ // Use prompt.details to grant exactly what the provider requires
881
+ const details = interaction.prompt.details;
882
+ // Grant missing OIDC scopes
883
+ if (details.missingOIDCScope) {
884
+ const scopes = details.missingOIDCScope;
885
+ grant.addOIDCScope([...scopes].join(' '));
886
+ }
887
+ // Grant missing OIDC claims
888
+ if (details.missingOIDCClaims) {
889
+ const claims = details.missingOIDCClaims;
890
+ grant.addOIDCClaims([...claims]);
891
+ }
892
+ // Grant missing resource scopes
893
+ if (details.missingResourceScopes) {
894
+ for (const [indicator, scopes] of Object.entries(details.missingResourceScopes)) {
895
+ grant.addResourceScope(indicator, [...scopes].join(' '));
896
+ }
897
+ }
898
+ // Explicitly grant offline_access if requested (see showConsent for details)
899
+ const requestedScope = interaction.params.scope ?? '';
900
+ if (requestedScope.includes('offline_access')) {
901
+ grant.addOIDCScope('offline_access');
902
+ }
903
+ const grantId = await grant.save();
904
+ writeAuditLog({
905
+ organizationId: org.id,
906
+ userId: interaction.session?.accountId,
907
+ eventType: 'user.consent.granted',
908
+ eventCategory: 'authentication',
909
+ description: `Consent granted for client ${clientId}`,
910
+ });
911
+ const result = {
912
+ consent: { grantId },
913
+ };
914
+ await provider.interactionFinished(ctx.req, ctx.res, result, {
915
+ mergeWithLastSubmission: true,
916
+ });
917
+ }
918
+ else {
919
+ // Deny consent
920
+ writeAuditLog({
921
+ organizationId: org.id,
922
+ userId: interaction.session?.accountId,
923
+ eventType: 'user.consent.denied',
924
+ eventCategory: 'authentication',
925
+ description: `Consent denied for client ${interaction.params.client_id}`,
926
+ });
927
+ const result = {
928
+ error: 'access_denied',
929
+ error_description: 'User denied consent',
930
+ };
931
+ await provider.interactionFinished(ctx.req, ctx.res, result, {
932
+ mergeWithLastSubmission: false,
933
+ });
934
+ }
935
+ }
936
+ catch {
937
+ logger.error({ event: 'consent-processing-failed' }, 'Consent processing failed');
938
+ await renderErrorPage(ctx, 'errors.interaction_expired');
939
+ }
940
+ }
941
+ /**
942
+ * Abort an OIDC interaction.
943
+ *
944
+ * Finishes the interaction with an access_denied error, effectively
945
+ * cancelling the authorization flow and redirecting back to the client.
946
+ *
947
+ * @param ctx - Koa context with organization state
948
+ * @param provider - OIDC provider instance
949
+ */
950
+ async function abortInteraction(ctx, provider) {
951
+ try {
952
+ const interaction = await provider.interactionDetails(ctx.req, ctx.res);
953
+ // Resolve organization from the interaction's client_id
954
+ await resolveOrganizationForInteraction(ctx, interaction.params.client_id, interaction.uid);
955
+ const org = ctx.state.organization;
956
+ writeAuditLog({
957
+ organizationId: org.id,
958
+ userId: interaction.session?.accountId,
959
+ eventType: 'user.consent.denied',
960
+ eventCategory: 'authentication',
961
+ description: 'Interaction aborted by user',
962
+ });
963
+ const result = {
964
+ error: 'access_denied',
965
+ error_description: 'User aborted the interaction',
966
+ };
967
+ await provider.interactionFinished(ctx.req, ctx.res, result, {
968
+ mergeWithLastSubmission: false,
969
+ });
970
+ }
971
+ catch {
972
+ logger.error({ event: 'interaction-abort-failed' }, 'Interaction abort failed');
973
+ await renderErrorPage(ctx, 'errors.interaction_expired');
974
+ }
975
+ }
976
+ // ---------------------------------------------------------------------------
977
+ // Helper: render login page with error
978
+ // ---------------------------------------------------------------------------
979
+ /**
980
+ * Render the login page with an error flash message.
981
+ * Used when login fails (wrong password, rate limited, CSRF mismatch, etc.).
982
+ *
983
+ * @param ctx - Koa context
984
+ * @param provider - OIDC provider instance (used to resolve human-readable client name)
985
+ * @param interaction - OIDC interaction details
986
+ * @param t - Translation function
987
+ * @param locale - Resolved locale
988
+ * @param email - Pre-filled email value
989
+ * @param errorMessage - Error message to display
990
+ * @param statusCode - HTTP status code (default: 200)
991
+ */
992
+ async function renderLoginWithError(ctx, provider, interaction, t, locale, email, errorMessage, statusCode = 200, dependencies = defaultEnumerationDependencies) {
993
+ const org = ctx.state.organization;
994
+ const csrfToken = dependencies.generateCsrfToken();
995
+ dependencies.setCsrfCookie(ctx, csrfToken);
996
+ // Resolve human-readable client name from provider metadata.
997
+ // Falls back to raw client_id if the client is not found.
998
+ const clientId = interaction.params.client_id ?? '';
999
+ const oidcClient = await provider.Client.find(clientId);
1000
+ const clientName = oidcClient?.metadata()?.client_name ?? clientId;
1001
+ // Resolve effective login methods so the re-rendered login page shows
1002
+ // the correct forms (password, magic-link, or both). Without these flags,
1003
+ // the template's no-methods fallback would incorrectly appear alongside
1004
+ // the flash error message.
1005
+ const effectiveMethods = oidcClient
1006
+ ? resolveLoginMethodsFromOidcClient(oidcClient, org)
1007
+ : org.defaultLoginMethods;
1008
+ const showPassword = effectiveMethods.includes('password');
1009
+ const showMagicLink = effectiveMethods.includes('magic_link');
1010
+ const context = {
1011
+ ...buildBaseContext(ctx, locale, csrfToken, org.slug),
1012
+ t,
1013
+ interaction: {
1014
+ uid: interaction.uid,
1015
+ prompt: interaction.prompt.name,
1016
+ params: interaction.params,
1017
+ client: { clientName },
1018
+ },
1019
+ email,
1020
+ flash: { error: errorMessage },
1021
+ // Login method rendering flags — must match showLogin() to avoid
1022
+ // the "no methods configured" fallback appearing on error re-renders.
1023
+ showPassword,
1024
+ showMagicLink,
1025
+ showDivider: showPassword && showMagicLink,
1026
+ loginMethods: effectiveMethods,
1027
+ };
1028
+ await renderAndRespond(ctx, 'login', context, statusCode, dependencies.renderPage);
1029
+ }
1030
+ // ---------------------------------------------------------------------------
1031
+ // Helper: render error page
1032
+ // ---------------------------------------------------------------------------
1033
+ /**
1034
+ * Render a generic error page when the interaction cannot be loaded.
1035
+ * Used as a fallback when the interaction has expired or is invalid.
1036
+ *
1037
+ * @param ctx - Koa context
1038
+ * @param errorKey - Translation key for the error message
1039
+ */
1040
+ async function renderErrorPage(ctx, errorKey) {
1041
+ try {
1042
+ const org = ctx.state.organization;
1043
+ const orgSlug = org?.slug ?? 'default';
1044
+ const locale = org
1045
+ ? await resolveLocale(undefined, ctx.get('Accept-Language') || undefined, org.defaultLocale)
1046
+ : 'en';
1047
+ const t = getTranslationFunction(locale, orgSlug);
1048
+ const csrfToken = generateCsrfToken();
1049
+ const context = {
1050
+ branding: org
1051
+ ? buildBrandingFromOrg(org)
1052
+ : {
1053
+ logoUrl: null,
1054
+ faviconUrl: null,
1055
+ primaryColor: '#3B82F6',
1056
+ companyName: 'Porta',
1057
+ customCss: null,
1058
+ },
1059
+ locale,
1060
+ t,
1061
+ csrfToken,
1062
+ orgSlug,
1063
+ errorMessage: t(errorKey),
1064
+ };
1065
+ await renderAndRespond(ctx, 'error', context, 400);
1066
+ }
1067
+ catch {
1068
+ // Last resort: if even the error page fails, send plain text
1069
+ logger.error({ event: 'interaction-error-render-failed' }, 'Interaction error page failed');
1070
+ ctx.status = 500;
1071
+ ctx.body = 'An error occurred';
1072
+ }
1073
+ }
1074
+ //# sourceMappingURL=interactions.js.map