@contember/engine-tenant-api 2.2.0-alpha.5 → 2.2.0-alpha.7

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 (245) hide show
  1. package/CLAUDE.md +1 -0
  2. package/dist/development/index.cjs +2 -0
  3. package/dist/development/index.cjs.map +1 -1
  4. package/dist/development/index.js +2 -0
  5. package/dist/development/index.js.map +1 -1
  6. package/dist/development/src/migrations/2026-09-04-120000-idp-disable-local-authentication.cjs +18 -0
  7. package/dist/development/src/migrations/2026-09-04-120000-idp-disable-local-authentication.cjs.map +1 -0
  8. package/dist/development/src/migrations/2026-09-04-120000-idp-disable-local-authentication.js +19 -0
  9. package/dist/development/src/migrations/2026-09-04-120000-idp-disable-local-authentication.js.map +1 -0
  10. package/dist/development/src/migrations/runner.cjs +3 -1
  11. package/dist/development/src/migrations/runner.cjs.map +1 -1
  12. package/dist/development/src/migrations/runner.js +3 -1
  13. package/dist/development/src/migrations/runner.js.map +1 -1
  14. package/dist/development/src/migrations/snapshot.cjs +2 -1
  15. package/dist/development/src/migrations/snapshot.cjs.map +1 -1
  16. package/dist/development/src/migrations/snapshot.js +2 -1
  17. package/dist/development/src/migrations/snapshot.js.map +1 -1
  18. package/dist/development/src/model/commands/idp/ClaimIdpRevalidationCommand.cjs +3 -7
  19. package/dist/development/src/model/commands/idp/ClaimIdpRevalidationCommand.cjs.map +1 -1
  20. package/dist/development/src/model/commands/idp/ClaimIdpRevalidationCommand.js +3 -7
  21. package/dist/development/src/model/commands/idp/ClaimIdpRevalidationCommand.js.map +1 -1
  22. package/dist/development/src/model/commands/idp/CreateIdpCommand.cjs +8 -2
  23. package/dist/development/src/model/commands/idp/CreateIdpCommand.cjs.map +1 -1
  24. package/dist/development/src/model/commands/idp/CreateIdpCommand.js +8 -2
  25. package/dist/development/src/model/commands/idp/CreateIdpCommand.js.map +1 -1
  26. package/dist/development/src/model/commands/idp/UpdateIdpCommand.cjs +6 -2
  27. package/dist/development/src/model/commands/idp/UpdateIdpCommand.cjs.map +1 -1
  28. package/dist/development/src/model/commands/idp/UpdateIdpCommand.js +6 -2
  29. package/dist/development/src/model/commands/idp/UpdateIdpCommand.js.map +1 -1
  30. package/dist/development/src/model/queries/idp/LocalAuthDisablingIdpsQuery.cjs +21 -0
  31. package/dist/development/src/model/queries/idp/LocalAuthDisablingIdpsQuery.cjs.map +1 -0
  32. package/dist/development/src/model/queries/idp/LocalAuthDisablingIdpsQuery.js +21 -0
  33. package/dist/development/src/model/queries/idp/LocalAuthDisablingIdpsQuery.js.map +1 -0
  34. package/dist/development/src/model/queries/idp/PersonIdentityProvidersQuery.cjs +1 -1
  35. package/dist/development/src/model/queries/idp/PersonIdentityProvidersQuery.cjs.map +1 -1
  36. package/dist/development/src/model/queries/idp/PersonIdentityProvidersQuery.js +1 -1
  37. package/dist/development/src/model/queries/idp/PersonIdentityProvidersQuery.js.map +1 -1
  38. package/dist/development/src/model/queries/idp/helpers.cjs +3 -3
  39. package/dist/development/src/model/queries/idp/helpers.cjs.map +1 -1
  40. package/dist/development/src/model/queries/idp/helpers.js +3 -3
  41. package/dist/development/src/model/queries/idp/helpers.js.map +1 -1
  42. package/dist/development/src/model/service/PasswordResetManager.cjs +5 -0
  43. package/dist/development/src/model/service/PasswordResetManager.cjs.map +1 -1
  44. package/dist/development/src/model/service/PasswordResetManager.js +5 -0
  45. package/dist/development/src/model/service/PasswordResetManager.js.map +1 -1
  46. package/dist/development/src/model/service/PasswordlessSignInManager.cjs +19 -0
  47. package/dist/development/src/model/service/PasswordlessSignInManager.cjs.map +1 -1
  48. package/dist/development/src/model/service/PasswordlessSignInManager.js +19 -0
  49. package/dist/development/src/model/service/PasswordlessSignInManager.js.map +1 -1
  50. package/dist/development/src/model/service/SignInManager.cjs +5 -0
  51. package/dist/development/src/model/service/SignInManager.cjs.map +1 -1
  52. package/dist/development/src/model/service/SignInManager.js +5 -0
  53. package/dist/development/src/model/service/SignInManager.js.map +1 -1
  54. package/dist/development/src/model/service/idp/PersonIdentityProviderManager.cjs +6 -0
  55. package/dist/development/src/model/service/idp/PersonIdentityProviderManager.cjs.map +1 -1
  56. package/dist/development/src/model/service/idp/PersonIdentityProviderManager.js +6 -0
  57. package/dist/development/src/model/service/idp/PersonIdentityProviderManager.js.map +1 -1
  58. package/dist/development/src/resolvers/mutation/idp/AddIDPMutationResolver.cjs +2 -1
  59. package/dist/development/src/resolvers/mutation/idp/AddIDPMutationResolver.cjs.map +1 -1
  60. package/dist/development/src/resolvers/mutation/idp/AddIDPMutationResolver.js +2 -1
  61. package/dist/development/src/resolvers/mutation/idp/AddIDPMutationResolver.js.map +1 -1
  62. package/dist/development/src/resolvers/mutation/idp/UpdateIDPMutationResolver.cjs +2 -1
  63. package/dist/development/src/resolvers/mutation/idp/UpdateIDPMutationResolver.cjs.map +1 -1
  64. package/dist/development/src/resolvers/mutation/idp/UpdateIDPMutationResolver.js +2 -1
  65. package/dist/development/src/resolvers/mutation/idp/UpdateIDPMutationResolver.js.map +1 -1
  66. package/dist/development/src/resolvers/mutation/idp/audit.cjs +1 -0
  67. package/dist/development/src/resolvers/mutation/idp/audit.cjs.map +1 -1
  68. package/dist/development/src/resolvers/mutation/idp/audit.js +1 -0
  69. package/dist/development/src/resolvers/mutation/idp/audit.js.map +1 -1
  70. package/dist/development/src/resolvers/mutation/person/PasswordlessMutationResolver.cjs +4 -0
  71. package/dist/development/src/resolvers/mutation/person/PasswordlessMutationResolver.cjs.map +1 -1
  72. package/dist/development/src/resolvers/mutation/person/PasswordlessMutationResolver.js +4 -0
  73. package/dist/development/src/resolvers/mutation/person/PasswordlessMutationResolver.js.map +1 -1
  74. package/dist/development/src/resolvers/mutation/person/SignInMutationResolver.cjs +2 -2
  75. package/dist/development/src/resolvers/mutation/person/SignInMutationResolver.cjs.map +1 -1
  76. package/dist/development/src/resolvers/mutation/person/SignInMutationResolver.js +2 -2
  77. package/dist/development/src/resolvers/mutation/person/SignInMutationResolver.js.map +1 -1
  78. package/dist/development/src/schema/tenant.graphql.cjs +28 -0
  79. package/dist/development/src/schema/tenant.graphql.cjs.map +1 -1
  80. package/dist/development/src/schema/tenant.graphql.js +28 -0
  81. package/dist/development/src/schema/tenant.graphql.js.map +1 -1
  82. package/dist/production/index.cjs +2 -0
  83. package/dist/production/index.cjs.map +1 -1
  84. package/dist/production/index.js +2 -0
  85. package/dist/production/index.js.map +1 -1
  86. package/dist/production/src/migrations/2026-09-04-120000-idp-disable-local-authentication.cjs +18 -0
  87. package/dist/production/src/migrations/2026-09-04-120000-idp-disable-local-authentication.cjs.map +1 -0
  88. package/dist/production/src/migrations/2026-09-04-120000-idp-disable-local-authentication.js +19 -0
  89. package/dist/production/src/migrations/2026-09-04-120000-idp-disable-local-authentication.js.map +1 -0
  90. package/dist/production/src/migrations/runner.cjs +3 -1
  91. package/dist/production/src/migrations/runner.cjs.map +1 -1
  92. package/dist/production/src/migrations/runner.js +3 -1
  93. package/dist/production/src/migrations/runner.js.map +1 -1
  94. package/dist/production/src/migrations/snapshot.cjs +2 -1
  95. package/dist/production/src/migrations/snapshot.cjs.map +1 -1
  96. package/dist/production/src/migrations/snapshot.js +2 -1
  97. package/dist/production/src/migrations/snapshot.js.map +1 -1
  98. package/dist/production/src/model/commands/idp/ClaimIdpRevalidationCommand.cjs +3 -7
  99. package/dist/production/src/model/commands/idp/ClaimIdpRevalidationCommand.cjs.map +1 -1
  100. package/dist/production/src/model/commands/idp/ClaimIdpRevalidationCommand.js +3 -7
  101. package/dist/production/src/model/commands/idp/ClaimIdpRevalidationCommand.js.map +1 -1
  102. package/dist/production/src/model/commands/idp/CreateIdpCommand.cjs +8 -2
  103. package/dist/production/src/model/commands/idp/CreateIdpCommand.cjs.map +1 -1
  104. package/dist/production/src/model/commands/idp/CreateIdpCommand.js +8 -2
  105. package/dist/production/src/model/commands/idp/CreateIdpCommand.js.map +1 -1
  106. package/dist/production/src/model/commands/idp/UpdateIdpCommand.cjs +6 -2
  107. package/dist/production/src/model/commands/idp/UpdateIdpCommand.cjs.map +1 -1
  108. package/dist/production/src/model/commands/idp/UpdateIdpCommand.js +6 -2
  109. package/dist/production/src/model/commands/idp/UpdateIdpCommand.js.map +1 -1
  110. package/dist/production/src/model/queries/idp/LocalAuthDisablingIdpsQuery.cjs +21 -0
  111. package/dist/production/src/model/queries/idp/LocalAuthDisablingIdpsQuery.cjs.map +1 -0
  112. package/dist/production/src/model/queries/idp/LocalAuthDisablingIdpsQuery.js +21 -0
  113. package/dist/production/src/model/queries/idp/LocalAuthDisablingIdpsQuery.js.map +1 -0
  114. package/dist/production/src/model/queries/idp/PersonIdentityProvidersQuery.cjs +1 -1
  115. package/dist/production/src/model/queries/idp/PersonIdentityProvidersQuery.cjs.map +1 -1
  116. package/dist/production/src/model/queries/idp/PersonIdentityProvidersQuery.js +1 -1
  117. package/dist/production/src/model/queries/idp/PersonIdentityProvidersQuery.js.map +1 -1
  118. package/dist/production/src/model/queries/idp/helpers.cjs +3 -3
  119. package/dist/production/src/model/queries/idp/helpers.cjs.map +1 -1
  120. package/dist/production/src/model/queries/idp/helpers.js +3 -3
  121. package/dist/production/src/model/queries/idp/helpers.js.map +1 -1
  122. package/dist/production/src/model/service/PasswordResetManager.cjs +5 -0
  123. package/dist/production/src/model/service/PasswordResetManager.cjs.map +1 -1
  124. package/dist/production/src/model/service/PasswordResetManager.js +5 -0
  125. package/dist/production/src/model/service/PasswordResetManager.js.map +1 -1
  126. package/dist/production/src/model/service/PasswordlessSignInManager.cjs +19 -0
  127. package/dist/production/src/model/service/PasswordlessSignInManager.cjs.map +1 -1
  128. package/dist/production/src/model/service/PasswordlessSignInManager.js +19 -0
  129. package/dist/production/src/model/service/PasswordlessSignInManager.js.map +1 -1
  130. package/dist/production/src/model/service/SignInManager.cjs +5 -0
  131. package/dist/production/src/model/service/SignInManager.cjs.map +1 -1
  132. package/dist/production/src/model/service/SignInManager.js +5 -0
  133. package/dist/production/src/model/service/SignInManager.js.map +1 -1
  134. package/dist/production/src/model/service/idp/PersonIdentityProviderManager.cjs +6 -0
  135. package/dist/production/src/model/service/idp/PersonIdentityProviderManager.cjs.map +1 -1
  136. package/dist/production/src/model/service/idp/PersonIdentityProviderManager.js +6 -0
  137. package/dist/production/src/model/service/idp/PersonIdentityProviderManager.js.map +1 -1
  138. package/dist/production/src/resolvers/mutation/idp/AddIDPMutationResolver.cjs +2 -1
  139. package/dist/production/src/resolvers/mutation/idp/AddIDPMutationResolver.cjs.map +1 -1
  140. package/dist/production/src/resolvers/mutation/idp/AddIDPMutationResolver.js +2 -1
  141. package/dist/production/src/resolvers/mutation/idp/AddIDPMutationResolver.js.map +1 -1
  142. package/dist/production/src/resolvers/mutation/idp/UpdateIDPMutationResolver.cjs +2 -1
  143. package/dist/production/src/resolvers/mutation/idp/UpdateIDPMutationResolver.cjs.map +1 -1
  144. package/dist/production/src/resolvers/mutation/idp/UpdateIDPMutationResolver.js +2 -1
  145. package/dist/production/src/resolvers/mutation/idp/UpdateIDPMutationResolver.js.map +1 -1
  146. package/dist/production/src/resolvers/mutation/idp/audit.cjs +1 -0
  147. package/dist/production/src/resolvers/mutation/idp/audit.cjs.map +1 -1
  148. package/dist/production/src/resolvers/mutation/idp/audit.js +1 -0
  149. package/dist/production/src/resolvers/mutation/idp/audit.js.map +1 -1
  150. package/dist/production/src/resolvers/mutation/person/PasswordlessMutationResolver.cjs +4 -0
  151. package/dist/production/src/resolvers/mutation/person/PasswordlessMutationResolver.cjs.map +1 -1
  152. package/dist/production/src/resolvers/mutation/person/PasswordlessMutationResolver.js +4 -0
  153. package/dist/production/src/resolvers/mutation/person/PasswordlessMutationResolver.js.map +1 -1
  154. package/dist/production/src/resolvers/mutation/person/SignInMutationResolver.cjs +2 -2
  155. package/dist/production/src/resolvers/mutation/person/SignInMutationResolver.cjs.map +1 -1
  156. package/dist/production/src/resolvers/mutation/person/SignInMutationResolver.js +2 -2
  157. package/dist/production/src/resolvers/mutation/person/SignInMutationResolver.js.map +1 -1
  158. package/dist/production/src/schema/tenant.graphql.cjs +28 -0
  159. package/dist/production/src/schema/tenant.graphql.cjs.map +1 -1
  160. package/dist/production/src/schema/tenant.graphql.js +28 -0
  161. package/dist/production/src/schema/tenant.graphql.js.map +1 -1
  162. package/dist/tests/cases/integration/mocked/gql/passwordless.d.ts +3 -0
  163. package/dist/tests/cases/integration/mocked/gql/passwordless.d.ts.map +1 -1
  164. package/dist/tests/cases/integration/mocked/localAuthenticationDisabled.test.d.ts +2 -0
  165. package/dist/tests/cases/integration/mocked/localAuthenticationDisabled.test.d.ts.map +1 -0
  166. package/dist/tests/cases/integration/mocked/sql/localAuthDisablingIdpsSql.d.ts +10 -0
  167. package/dist/tests/cases/integration/mocked/sql/localAuthDisablingIdpsSql.d.ts.map +1 -0
  168. package/dist/tests/cases/integration/mocked/sql/personIdentityProvidersSql.d.ts +1 -0
  169. package/dist/tests/cases/integration/mocked/sql/personIdentityProvidersSql.d.ts.map +1 -1
  170. package/dist/tests/cases/unit/idp/claimIdpRevalidation.test.d.ts +2 -0
  171. package/dist/tests/cases/unit/idp/claimIdpRevalidation.test.d.ts.map +1 -0
  172. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  173. package/dist/types/migrations/2026-09-04-120000-idp-disable-local-authentication.d.ts +3 -0
  174. package/dist/types/migrations/2026-09-04-120000-idp-disable-local-authentication.d.ts.map +1 -0
  175. package/dist/types/migrations/runner.d.ts.map +1 -1
  176. package/dist/types/migrations/snapshot.d.ts.map +1 -1
  177. package/dist/types/model/commands/idp/ClaimIdpRevalidationCommand.d.ts.map +1 -1
  178. package/dist/types/model/commands/idp/CreateIdpCommand.d.ts.map +1 -1
  179. package/dist/types/model/commands/idp/UpdateIdpCommand.d.ts.map +1 -1
  180. package/dist/types/model/queries/idp/LocalAuthDisablingIdpsQuery.d.ts +18 -0
  181. package/dist/types/model/queries/idp/LocalAuthDisablingIdpsQuery.d.ts.map +1 -0
  182. package/dist/types/model/queries/idp/PersonIdentityProvidersQuery.d.ts +1 -0
  183. package/dist/types/model/queries/idp/PersonIdentityProvidersQuery.d.ts.map +1 -1
  184. package/dist/types/model/queries/idp/helpers.d.ts +1 -1
  185. package/dist/types/model/queries/idp/helpers.d.ts.map +1 -1
  186. package/dist/types/model/queries/idp/index.d.ts +1 -0
  187. package/dist/types/model/queries/idp/index.d.ts.map +1 -1
  188. package/dist/types/model/queries/idp/types.d.ts +1 -0
  189. package/dist/types/model/queries/idp/types.d.ts.map +1 -1
  190. package/dist/types/model/service/PasswordResetManager.d.ts.map +1 -1
  191. package/dist/types/model/service/PasswordlessSignInManager.d.ts.map +1 -1
  192. package/dist/types/model/service/SignInManager.d.ts.map +1 -1
  193. package/dist/types/model/service/idp/PersonIdentityProviderManager.d.ts.map +1 -1
  194. package/dist/types/model/type/IdentityProvider.d.ts +2 -0
  195. package/dist/types/model/type/IdentityProvider.d.ts.map +1 -1
  196. package/dist/types/resolvers/mutation/idp/AddIDPMutationResolver.d.ts.map +1 -1
  197. package/dist/types/resolvers/mutation/idp/UpdateIDPMutationResolver.d.ts.map +1 -1
  198. package/dist/types/resolvers/mutation/idp/audit.d.ts.map +1 -1
  199. package/dist/types/resolvers/mutation/person/PasswordlessMutationResolver.d.ts.map +1 -1
  200. package/dist/types/schema/index.d.ts +33 -4
  201. package/dist/types/schema/index.d.ts.map +1 -1
  202. package/dist/types/schema/tenant.graphql.d.ts.map +1 -1
  203. package/dist/types/tsconfig.tsbuildinfo +1 -1
  204. package/package.json +12 -12
  205. package/src/migrations/2026-09-04-120000-idp-disable-local-authentication.ts +18 -0
  206. package/src/migrations/runner.ts +2 -0
  207. package/src/migrations/snapshot.ts +2 -1
  208. package/src/model/commands/idp/ClaimIdpRevalidationCommand.ts +8 -7
  209. package/src/model/commands/idp/CreateIdpCommand.ts +7 -1
  210. package/src/model/commands/idp/UpdateIdpCommand.ts +5 -1
  211. package/src/model/queries/idp/LocalAuthDisablingIdpsQuery.ts +41 -0
  212. package/src/model/queries/idp/PersonIdentityProvidersQuery.ts +2 -0
  213. package/src/model/queries/idp/helpers.ts +3 -2
  214. package/src/model/queries/idp/index.ts +1 -0
  215. package/src/model/queries/idp/types.ts +1 -0
  216. package/src/model/service/PasswordResetManager.ts +9 -1
  217. package/src/model/service/PasswordlessSignInManager.ts +23 -1
  218. package/src/model/service/SignInManager.ts +9 -1
  219. package/src/model/service/idp/PersonIdentityProviderManager.ts +8 -0
  220. package/src/model/type/IdentityProvider.ts +2 -0
  221. package/src/resolvers/mutation/idp/AddIDPMutationResolver.ts +1 -0
  222. package/src/resolvers/mutation/idp/UpdateIDPMutationResolver.ts +1 -0
  223. package/src/resolvers/mutation/idp/audit.ts +1 -0
  224. package/src/resolvers/mutation/person/PasswordlessMutationResolver.ts +7 -0
  225. package/src/resolvers/mutation/person/SignInMutationResolver.ts +2 -2
  226. package/src/schema/index.ts +29 -0
  227. package/src/schema/tenant.graphql.ts +28 -0
  228. package/tests/cases/integration/mocked/gql/passwordless.ts +10 -0
  229. package/tests/cases/integration/mocked/idpClaimMappingConfig.test.ts +4 -4
  230. package/tests/cases/integration/mocked/localAuthenticationDisabled.test.ts +182 -0
  231. package/tests/cases/integration/mocked/personIdentityProviders.test.ts +67 -0
  232. package/tests/cases/integration/mocked/signInAnomalyDetection.test.ts +11 -0
  233. package/tests/cases/integration/mocked/signInIdpClaimMapping.test.ts +14 -0
  234. package/tests/cases/integration/mocked/signInIdpClaimMappingFixes.test.ts +4 -0
  235. package/tests/cases/integration/mocked/signInIdpClaimMappingSafety.test.ts +6 -0
  236. package/tests/cases/integration/mocked/signInIdpMutation.test.ts +5 -0
  237. package/tests/cases/integration/mocked/signInMfaEnforcement.test.ts +7 -0
  238. package/tests/cases/integration/mocked/signInMutation.test.ts +14 -0
  239. package/tests/cases/integration/mocked/signInPasswordlessMutation.test.ts +36 -0
  240. package/tests/cases/integration/mocked/sql/getIdpBySlugSql.ts +1 -1
  241. package/tests/cases/integration/mocked/sql/localAuthDisablingIdpsSql.ts +21 -0
  242. package/tests/cases/integration/mocked/sql/personIdentityProvidersSql.ts +3 -1
  243. package/tests/cases/integration/mocked/trustForwardedClientInfo.test.ts +3 -0
  244. package/tests/cases/unit/idp/claimIdpRevalidation.test.ts +50 -0
  245. package/tests/cases/unit/idp/idpManagerUnknownConfigKeys.test.ts +8 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contember/engine-tenant-api",
3
- "version": "2.2.0-alpha.5",
3
+ "version": "2.2.0-alpha.7",
4
4
  "license": "Apache-2.0",
5
5
  "main": "./dist/production/index.js",
6
6
  "typings": "./dist/types/index.d.ts",
@@ -26,16 +26,16 @@
26
26
  "generate": "gql-gen --config graphql.codegen.yml"
27
27
  },
28
28
  "dependencies": {
29
- "@contember/authorization": "2.2.0-alpha.5",
30
- "@contember/config-loader": "2.2.0-alpha.5",
31
- "@contember/database": "2.2.0-alpha.5",
32
- "@contember/database-migrations": "2.2.0-alpha.5",
33
- "@contember/dic": "2.2.0-alpha.5",
34
- "@contember/graphql-utils": "2.2.0-alpha.5",
35
- "@contember/logger": "2.2.0-alpha.5",
36
- "@contember/schema": "2.2.0-alpha.5",
37
- "@contember/schema-utils": "2.2.0-alpha.5",
38
- "@contember/typesafe": "2.2.0-alpha.5",
29
+ "@contember/authorization": "2.2.0-alpha.7",
30
+ "@contember/config-loader": "2.2.0-alpha.7",
31
+ "@contember/database": "2.2.0-alpha.7",
32
+ "@contember/database-migrations": "2.2.0-alpha.7",
33
+ "@contember/dic": "2.2.0-alpha.7",
34
+ "@contember/graphql-utils": "2.2.0-alpha.7",
35
+ "@contember/logger": "2.2.0-alpha.7",
36
+ "@contember/schema": "2.2.0-alpha.7",
37
+ "@contember/schema-utils": "2.2.0-alpha.7",
38
+ "@contember/typesafe": "2.2.0-alpha.7",
39
39
  "@graphql-tools/utils": "^11.0.0",
40
40
  "graphql-tag": "^2.12.6",
41
41
  "jose": "^6.2.1",
@@ -46,7 +46,7 @@
46
46
  "otpauth": "^9.5.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@contember/database-tester": "2.2.0-alpha.5",
49
+ "@contember/database-tester": "2.2.0-alpha.7",
50
50
  "@graphql-tools/schema": "^10.0.31",
51
51
  "@types/mustache": "^4.2.6",
52
52
  "@types/node": "^24.12.0",
@@ -0,0 +1,18 @@
1
+ import { MigrationBuilder } from '@contember/database-migrations'
2
+
3
+ const sql = `
4
+ -- When TRUE, a person linked to this identity provider may authenticate ONLY through
5
+ -- it: password sign-in, passwordless sign-in and password-reset mails are refused for
6
+ -- them. The link itself is the marker, so it also covers a person who predates the
7
+ -- provider and got linked by e-mail matching.
8
+ --
9
+ -- Scoped to providers that are currently enabled: disabling the provider restores
10
+ -- local authentication for everyone linked to it, which is the break-glass for an IdP
11
+ -- outage. Defaults to FALSE to preserve existing behavior.
12
+ ALTER TABLE "identity_provider"
13
+ ADD COLUMN "disable_local_authentication" BOOLEAN NOT NULL DEFAULT FALSE;
14
+ `
15
+
16
+ export default async function(builder: MigrationBuilder) {
17
+ builder.sql(sql)
18
+ }
@@ -65,6 +65,7 @@ import _20260616130000idplogout from './2026-06-16-130000-idp-logout.js'
65
65
  import _20260618120000idpclaimmapping from './2026-06-18-120000-idp-claim-mapping.js'
66
66
  import _20260803120000personenableauthlog from './2026-08-03-120000-person-enable-auth-log.js'
67
67
  import _20260804120000configpanel from './2026-08-04-120000-config-panel.js'
68
+ import _20260904120000idpdisablelocalauthentication from './2026-09-04-120000-idp-disable-local-authentication.js'
68
69
  import snapshot from './snapshot.js'
69
70
  import { computeTokenHash, Providers } from '../model/index.js'
70
71
  import { Logger } from '@contember/logger'
@@ -153,6 +154,7 @@ const migrations = {
153
154
  '2026-06-18-120000-idp-claim-mapping': _20260618120000idpclaimmapping,
154
155
  '2026-08-03-120000-person-enable-auth-log': _20260803120000personenableauthlog,
155
156
  '2026-08-04-120000-config-panel': _20260804120000configpanel,
157
+ '2026-09-04-120000-idp-disable-local-authentication': _20260904120000idpdisablelocalauthentication,
156
158
  }
157
159
 
158
160
  export class TenantMigrationsRunner {
@@ -204,7 +204,8 @@ CREATE TABLE "identity_provider" (
204
204
  "exclusive" boolean DEFAULT false,
205
205
  "init_returns_config" boolean DEFAULT false NOT NULL,
206
206
  "require_verified_email" boolean DEFAULT false NOT NULL,
207
- "assume_email_verified" boolean DEFAULT false NOT NULL
207
+ "assume_email_verified" boolean DEFAULT false NOT NULL,
208
+ "disable_local_authentication" boolean DEFAULT false NOT NULL
208
209
  );
209
210
  CREATE TABLE "idp_session" (
210
211
  "id" "uuid" NOT NULL,
@@ -1,3 +1,4 @@
1
+ import { UpdateBuilder } from '@contember/database'
1
2
  import { Command } from '../Command.js'
2
3
 
3
4
  /**
@@ -17,12 +18,12 @@ export class ClaimIdpRevalidationCommand implements Command<boolean> {
17
18
  }
18
19
 
19
20
  async execute({ db }: Command.Args): Promise<boolean> {
20
- const result = await db.query(
21
- `UPDATE "idp_session"
22
- SET "last_validated_at" = now()
23
- WHERE "id" = ? AND "last_validated_at" <= now() - ?::interval`,
24
- [this.id, this.interval],
25
- )
26
- return (result.rowCount ?? 0) > 0
21
+ const qb = UpdateBuilder.create()
22
+ .table('idp_session')
23
+ .where({ id: this.id })
24
+ .where(expr => expr.raw('"last_validated_at" <= now() - ?::interval', this.interval))
25
+ .values({ last_validated_at: expr => expr.raw('now()') })
26
+
27
+ return (await qb.execute(db)) > 0
27
28
  }
28
29
  }
@@ -9,7 +9,12 @@ export class CreateIdpCommand implements Command<void> {
9
9
  }
10
10
 
11
11
  async execute({ db, providers }: Command.Args): Promise<void> {
12
- const { configuration, options: { autoSignUp, exclusive, initReturnsConfig, requireVerifiedEmail, assumeEmailVerified }, slug, type } = this.data
12
+ const {
13
+ configuration,
14
+ options: { autoSignUp, exclusive, initReturnsConfig, requireVerifiedEmail, assumeEmailVerified, disableLocalAuthentication },
15
+ slug,
16
+ type,
17
+ } = this.data
13
18
  await InsertBuilder.create()
14
19
  .into('identity_provider')
15
20
  .values({
@@ -22,6 +27,7 @@ export class CreateIdpCommand implements Command<void> {
22
27
  init_returns_config: initReturnsConfig,
23
28
  require_verified_email: requireVerifiedEmail,
24
29
  assume_email_verified: assumeEmailVerified,
30
+ disable_local_authentication: disableLocalAuthentication,
25
31
  })
26
32
  .execute(db)
27
33
  }
@@ -10,7 +10,10 @@ export class UpdateIdpCommand implements Command<void> {
10
10
  }
11
11
 
12
12
  async execute({ db }: Command.Args): Promise<void> {
13
- const { configuration, options: { autoSignUp, exclusive, initReturnsConfig, requireVerifiedEmail, assumeEmailVerified } = {} } = this.data
13
+ const {
14
+ configuration,
15
+ options: { autoSignUp, exclusive, initReturnsConfig, requireVerifiedEmail, assumeEmailVerified, disableLocalAuthentication } = {},
16
+ } = this.data
14
17
  await UpdateBuilder.create()
15
18
  .table('identity_provider')
16
19
  .values({
@@ -20,6 +23,7 @@ export class UpdateIdpCommand implements Command<void> {
20
23
  init_returns_config: initReturnsConfig,
21
24
  require_verified_email: requireVerifiedEmail,
22
25
  assume_email_verified: assumeEmailVerified,
26
+ disable_local_authentication: disableLocalAuthentication,
23
27
  })
24
28
  .where({
25
29
  id: this.id,
@@ -0,0 +1,41 @@
1
+ import { DatabaseQuery, DatabaseQueryable, Operator, SelectBuilder } from '@contember/database'
2
+
3
+ /**
4
+ * Slugs of the identity providers that forbid this person every local
5
+ * authentication method — password sign-in, passwordless sign-in and the
6
+ * password-reset mail. Empty means local authentication is available.
7
+ *
8
+ * The IdP link is the marker, so this also covers a person who predates the
9
+ * provider and was linked to it by e-mail matching, not only one created by
10
+ * auto-sign-up. A DISABLED provider does not count: turning the provider off
11
+ * hands local authentication back to everyone linked to it, which is the
12
+ * break-glass when the IdP is unreachable.
13
+ */
14
+ export class LocalAuthDisablingIdpsQuery extends DatabaseQuery<string[]> {
15
+ constructor(
16
+ private readonly personId: string,
17
+ ) {
18
+ super()
19
+ }
20
+
21
+ async fetch({ db }: DatabaseQueryable): Promise<string[]> {
22
+ const rows = await SelectBuilder.create<{ slug: string }>()
23
+ .select(['identity_provider', 'slug'], 'slug')
24
+ .from('person_identity_provider')
25
+ .join(
26
+ 'identity_provider',
27
+ 'identity_provider',
28
+ expr => expr.columnsEq(['identity_provider', 'id'], ['person_identity_provider', 'identity_provider_id']),
29
+ )
30
+ .where(where =>
31
+ where
32
+ .compare(['person_identity_provider', 'person_id'], Operator.eq, this.personId)
33
+ .compare(['identity_provider', 'disable_local_authentication'], Operator.eq, true)
34
+ .isNull(['identity_provider', 'disabled_at'])
35
+ )
36
+ .orderBy(['identity_provider', 'slug'])
37
+ .getResult(db)
38
+
39
+ return rows.map(it => it.slug)
40
+ }
41
+ }
@@ -7,6 +7,7 @@ export interface PersonIdentityProviderRow {
7
7
  readonly identityProviderSlug: string
8
8
  readonly identityProviderType: string
9
9
  readonly identityProviderDisabledAt: Date | null
10
+ readonly identityProviderDisableLocalAuthentication: boolean
10
11
  }
11
12
 
12
13
  /**
@@ -29,6 +30,7 @@ export class PersonIdentityProvidersQuery extends DatabaseQuery<PersonIdentityPr
29
30
  .select(['identity_provider', 'slug'], 'identityProviderSlug')
30
31
  .select(['identity_provider', 'type'], 'identityProviderType')
31
32
  .select(['identity_provider', 'disabled_at'], 'identityProviderDisabledAt')
33
+ .select(['identity_provider', 'disable_local_authentication'], 'identityProviderDisableLocalAuthentication')
32
34
  .from('person_identity_provider')
33
35
  .join(
34
36
  'identity_provider',
@@ -13,13 +13,14 @@ export const createBaseIdpQuery = () =>
13
13
  .select('init_returns_config', 'initReturnsConfig')
14
14
  .select('require_verified_email', 'requireVerifiedEmail')
15
15
  .select('assume_email_verified', 'assumeEmailVerified')
16
+ .select('disable_local_authentication', 'disableLocalAuthentication')
16
17
  .from('identity_provider')
17
18
 
18
19
  export const createIdpDto = (
19
- { exclusive, autoSignUp, initReturnsConfig, requireVerifiedEmail, assumeEmailVerified, ...row }: IdentityProviderRow,
20
+ { exclusive, autoSignUp, initReturnsConfig, requireVerifiedEmail, assumeEmailVerified, disableLocalAuthentication, ...row }: IdentityProviderRow,
20
21
  ): IdentityProviderDto => {
21
22
  return {
22
23
  ...row,
23
- options: { autoSignUp, exclusive, initReturnsConfig, requireVerifiedEmail, assumeEmailVerified },
24
+ options: { autoSignUp, exclusive, initReturnsConfig, requireVerifiedEmail, assumeEmailVerified, disableLocalAuthentication },
24
25
  }
25
26
  }
@@ -3,4 +3,5 @@ export * from './IdentityProvidersQuery.js'
3
3
  export * from './IdpSessionByApiKeyQuery.js'
4
4
  export * from './IdpSessionsBySidQuery.js'
5
5
  export * from './IdpSessionsBySubQuery.js'
6
+ export * from './LocalAuthDisablingIdpsQuery.js'
6
7
  export * from './PersonIdentityProvidersQuery.js'
@@ -11,6 +11,7 @@ export type IdentityProviderRow = {
11
11
  initReturnsConfig: boolean
12
12
  requireVerifiedEmail: boolean
13
13
  assumeEmailVerified: boolean
14
+ disableLocalAuthentication: boolean
14
15
  }
15
16
 
16
17
  export type IdentityProviderDto =
@@ -1,6 +1,6 @@
1
1
  import { Response, ResponseError, ResponseOk } from '../utils/Response.js'
2
2
  import { UserMailer } from '../mailing/index.js'
3
- import { ConfigurationQuery, PersonRow } from '../queries/index.js'
3
+ import { ConfigurationQuery, LocalAuthDisablingIdpsQuery, PersonRow } from '../queries/index.js'
4
4
  import { PermissionContext } from '../authorization/index.js'
5
5
  import { ProjectManager } from './ProjectManager.js'
6
6
  import { DatabaseContext } from '../utils/index.js'
@@ -35,6 +35,14 @@ export class PasswordResetManager {
35
35
  throw new ImplementationException()
36
36
  }
37
37
 
38
+ // A person federated through a provider with `disableLocalAuthentication`
39
+ // cannot sign in with a password, so a reset mail would only offer them a
40
+ // credential the tenant refuses — and one more link worth phishing.
41
+ const exclusiveIdps = await dbContext.queryHandler.fetch(new LocalAuthDisablingIdpsQuery(person.id))
42
+ if (exclusiveIdps.length > 0) {
43
+ return
44
+ }
45
+
38
46
  // Per-email exponential backoff on outbound mails — don't spam a real
39
47
  // user with reset mails just because attackers keep poking the
40
48
  // endpoint. Reuses login_* backoff config against person_auth_log.
@@ -1,5 +1,5 @@
1
1
  import { ApiKeyManager } from './apiKey/index.js'
2
- import { ConfigurationQuery, PersonQuery, PersonRow } from '../queries/index.js'
2
+ import { ConfigurationQuery, LocalAuthDisablingIdpsQuery, PersonQuery, PersonRow } from '../queries/index.js'
3
3
  import { Response, ResponseError, ResponseOk } from '../utils/Response.js'
4
4
  import {
5
5
  ActivatePasswordlessOtpErrorCode,
@@ -62,6 +62,16 @@ class PasswordlessSignInManager {
62
62
  }),
63
63
  })
64
64
  }
65
+ const exclusiveIdps = await db.queryHandler.fetch(new LocalAuthDisablingIdpsQuery(person.id))
66
+ if (exclusiveIdps.length > 0) {
67
+ return new ResponseError('IDP_REQUIRED', `Sign in using ${exclusiveIdps.join(' or ')}`, {
68
+ [AuthLogService.Key]: new AuthLogService.Bag({
69
+ personInput: email,
70
+ personId: person.id,
71
+ }),
72
+ })
73
+ }
74
+
65
75
  if (!isPasswordlessEnabled(configuration.passwordless.enabled, person.passwordless_enabled)) {
66
76
  return new ResponseError('PASSWORDLESS_DISABLED', 'Passwordless sign-in is disabled for this person', {
67
77
  [AuthLogService.Key]: new AuthLogService.Bag({
@@ -185,6 +195,18 @@ class PasswordlessSignInManager {
185
195
  })
186
196
  }
187
197
 
198
+ // Re-checked here and not only at init: a link created while this magic link
199
+ // was in flight would otherwise still mint a locally authenticated session.
200
+ const exclusiveIdps = await db.queryHandler.fetch(new LocalAuthDisablingIdpsQuery(personRow.id))
201
+ if (exclusiveIdps.length > 0) {
202
+ return new ResponseError('IDP_REQUIRED', `Sign in using ${exclusiveIdps.join(' or ')}`, {
203
+ [AuthLogService.Key]: new AuthLogService.Bag({
204
+ personId: personRow.id,
205
+ tokenId: tokenResult?.id,
206
+ }),
207
+ })
208
+ }
209
+
188
210
  let usedBackupCode = false
189
211
  if (personRow.otp_secret && personRow.otp_activated_at) {
190
212
  if (mfaOtp) {
@@ -1,6 +1,6 @@
1
1
  import { CreateSessionTokenErrorCode, MfaEnrollment, SignInErrorCode } from '../../schema/index.js'
2
2
  import { ApiKeyManager, AuthPolicyResolver, BackupCodeManager, EmailOtpManager, LoginRiskAnalyzer, OtpManager } from '../service/index.js'
3
- import { ConfigurationQuery, PersonQuery, PersonRow, PersonUniqueIdentifier } from '../queries/index.js'
3
+ import { ConfigurationQuery, LocalAuthDisablingIdpsQuery, PersonQuery, PersonRow, PersonUniqueIdentifier } from '../queries/index.js'
4
4
  import { Config } from '../type/Config.js'
5
5
  import { Providers } from '../providers.js'
6
6
  import { DatabaseContext } from '../utils/index.js'
@@ -51,6 +51,14 @@ class SignInManager {
51
51
  personId: person.id,
52
52
  }),
53
53
  }
54
+ // Refused before the password is even looked at: for a person federated
55
+ // through a provider with `disableLocalAuthentication`, a password is not a
56
+ // credential this tenant accepts, however it got set.
57
+ const exclusiveIdps = await dbContext.queryHandler.fetch(new LocalAuthDisablingIdpsQuery(person.id))
58
+ if (exclusiveIdps.length > 0) {
59
+ return new ResponseError('IDP_REQUIRED', `Sign in using ${exclusiveIdps.join(' or ')}`, authLogData)
60
+ }
61
+
54
62
  if (!person.password_hash) {
55
63
  return new ResponseError('NO_PASSWORD_SET', `No password set`, authLogData)
56
64
  }
@@ -29,6 +29,14 @@ export class PersonIdentityProviderManager {
29
29
  return new ResponseError('NOT_FOUND', `You are not connected to the identity provider connection "${connectionId}".`)
30
30
  }
31
31
 
32
+ // The link is what binds the person to the provider's policy, so they cannot remove it themselves.
33
+ if (connection.identityProviderDisableLocalAuthentication) {
34
+ return new ResponseError(
35
+ 'IDP_REQUIRED',
36
+ `The identity provider "${connection.identityProviderSlug}" is the only way you may sign in; its connection cannot be removed.`,
37
+ )
38
+ }
39
+
32
40
  const configuration = await db.queryHandler.fetch(new ConfigurationQuery(db.providers))
33
41
  const hasPassword = person.password_hash !== null
34
42
  const hasPasswordless = isPasswordlessEnabled(configuration.passwordless.enabled, person.passwordless_enabled)
@@ -11,4 +11,6 @@ export interface IdentityProviderOptions {
11
11
  initReturnsConfig: boolean
12
12
  requireVerifiedEmail: boolean
13
13
  assumeEmailVerified: boolean
14
+ /** A person linked to this provider may authenticate only through it. */
15
+ disableLocalAuthentication: boolean
14
16
  }
@@ -26,6 +26,7 @@ export class AddIDPMutationResolver implements MutationResolvers {
26
26
  initReturnsConfig: args.options?.initReturnsConfig ?? false,
27
27
  requireVerifiedEmail: args.options?.requireVerifiedEmail ?? false,
28
28
  assumeEmailVerified: args.options?.assumeEmailVerified ?? false,
29
+ disableLocalAuthentication: args.options?.disableLocalAuthentication ?? false,
29
30
  },
30
31
  })
31
32
  if (!result.ok) {
@@ -26,6 +26,7 @@ export class UpdateIDPMutationResolver implements MutationResolvers {
26
26
  initReturnsConfig: args.options?.initReturnsConfig ?? undefined,
27
27
  requireVerifiedEmail: args.options?.requireVerifiedEmail ?? undefined,
28
28
  assumeEmailVerified: args.options?.assumeEmailVerified ?? undefined,
29
+ disableLocalAuthentication: args.options?.disableLocalAuthentication ?? undefined,
29
30
  },
30
31
  }, args.mergeConfiguration ?? false)
31
32
  if (!result.ok) {
@@ -14,6 +14,7 @@ export const idpRowToAuditSnapshot = (row: IdentityProviderRow | null): JSONValu
14
14
  initReturnsConfig: row.initReturnsConfig,
15
15
  requireVerifiedEmail: row.requireVerifiedEmail,
16
16
  assumeEmailVerified: row.assumeEmailVerified,
17
+ disableLocalAuthentication: row.disableLocalAuthentication,
17
18
  configurationKeys: Object.keys(row.configuration).sort(),
18
19
  }
19
20
  }
@@ -68,6 +68,13 @@ export class PasswordlessMutationResolver
68
68
  // If the user does not exist, we don't want to reveal that
69
69
  return createErrorResponse('PASSWORDLESS_DISABLED', 'Passwordless sign-in is disabled for this person')
70
70
  }
71
+ // Naming the provider tells the caller both that the person exists and how
72
+ // they authenticate, so it needs both reveal flags. PASSWORDLESS_DISABLED is
73
+ // the neutral answer this endpoint already gives for an unknown address.
74
+ const { revealUserExists, revealLoginMethod } = configuration.login
75
+ if (result.error === 'IDP_REQUIRED' && !(revealUserExists && revealLoginMethod)) {
76
+ return createErrorResponse('PASSWORDLESS_DISABLED', 'Passwordless sign-in is disabled for this person')
77
+ }
71
78
 
72
79
  return createErrorResponse(result.error, result.errorMessage)
73
80
  }
@@ -108,10 +108,10 @@ export class SignInMutationResolver implements MutationResolvers {
108
108
  if (!response.ok) {
109
109
  const { revealUserExists, revealLoginMethod } = configuration.login
110
110
  const collapseAll = !revealUserExists
111
- && ['NO_PASSWORD_SET', 'PERSON_DISABLED', 'INVALID_PASSWORD', 'UNKNOWN_EMAIL'].includes(response.error)
111
+ && ['NO_PASSWORD_SET', 'PERSON_DISABLED', 'INVALID_PASSWORD', 'UNKNOWN_EMAIL', 'IDP_REQUIRED'].includes(response.error)
112
112
  const collapseMethodOnly = revealUserExists
113
113
  && !revealLoginMethod
114
- && ['NO_PASSWORD_SET', 'INVALID_PASSWORD'].includes(response.error)
114
+ && ['NO_PASSWORD_SET', 'INVALID_PASSWORD', 'IDP_REQUIRED'].includes(response.error)
115
115
  if (collapseAll || collapseMethodOnly) {
116
116
  return createErrorResponse('INVALID_CREDENTIALS', 'Invalid credentials')
117
117
  }
@@ -948,6 +948,11 @@ export type DisconnectIdpError = {
948
948
  }
949
949
 
950
950
  export type DisconnectIdpErrorCode =
951
+ /**
952
+ * The provider has `disableLocalAuthentication`, so this connection is the only way
953
+ * the person may sign in and cannot be removed by them — not even while the provider is disabled.
954
+ */
955
+ | 'IDP_REQUIRED'
951
956
  /** The person is not allowed to disconnect their last remaining sign-in method. */
952
957
  | 'LAST_AUTH_METHOD'
953
958
  /** The caller is not a person (e.g. an API key) and so has no IdP connections to disconnect. */
@@ -1013,6 +1018,7 @@ export type ForceSignOutPersonResponse = {
1013
1018
  export type IdpOptions = {
1014
1019
  readonly assumeEmailVerified?: InputMaybe<Scalars['Boolean']['input']>
1015
1020
  readonly autoSignUp?: InputMaybe<Scalars['Boolean']['input']>
1021
+ readonly disableLocalAuthentication?: InputMaybe<Scalars['Boolean']['input']>
1016
1022
  readonly exclusive?: InputMaybe<Scalars['Boolean']['input']>
1017
1023
  readonly initReturnsConfig?: InputMaybe<Scalars['Boolean']['input']>
1018
1024
  readonly requireVerifiedEmail?: InputMaybe<Scalars['Boolean']['input']>
@@ -1027,6 +1033,13 @@ export type IdpOptionsOutput = {
1027
1033
  */
1028
1034
  readonly assumeEmailVerified: Scalars['Boolean']['output']
1029
1035
  readonly autoSignUp: Scalars['Boolean']['output']
1036
+ /**
1037
+ * When true, a person linked to this provider may authenticate ONLY through it:
1038
+ * password sign-in and passwordless sign-in are refused and no password-reset
1039
+ * mail is sent. Disabling the provider lifts the restriction, which is the
1040
+ * break-glass for an IdP outage. Defaults to false.
1041
+ */
1042
+ readonly disableLocalAuthentication: Scalars['Boolean']['output']
1030
1043
  readonly exclusive: Scalars['Boolean']['output']
1031
1044
  readonly initReturnsConfig: Scalars['Boolean']['output']
1032
1045
  /**
@@ -1169,6 +1182,11 @@ export type InitSignInPasswordlessError = {
1169
1182
  }
1170
1183
 
1171
1184
  export type InitSignInPasswordlessErrorCode =
1185
+ /**
1186
+ * The person is linked to an identity provider with
1187
+ * `disableLocalAuthentication`, so passwordless sign-in is not available to them.
1188
+ */
1189
+ | 'IDP_REQUIRED'
1172
1190
  | 'INVALID_CAPTCHA'
1173
1191
  | 'PASSWORDLESS_DISABLED'
1174
1192
  | 'PERSON_NOT_FOUND'
@@ -2150,6 +2168,11 @@ export type SignInError = {
2150
2168
 
2151
2169
  export type SignInErrorCode =
2152
2170
  | 'EMAIL_NOT_VERIFIED'
2171
+ /**
2172
+ * The person is linked to an identity provider with
2173
+ * `disableLocalAuthentication`, so password sign-in is not available to them.
2174
+ */
2175
+ | 'IDP_REQUIRED'
2153
2176
  | 'INVALID_CREDENTIALS'
2154
2177
  | 'INVALID_OTP_TOKEN'
2155
2178
  | 'INVALID_PASSWORD'
@@ -2214,6 +2237,11 @@ export type SignInPasswordlessError = {
2214
2237
  }
2215
2238
 
2216
2239
  export type SignInPasswordlessErrorCode =
2240
+ /**
2241
+ * The person became linked to an identity provider with
2242
+ * `disableLocalAuthentication` after this magic link was issued.
2243
+ */
2244
+ | 'IDP_REQUIRED'
2217
2245
  | 'INVALID_OTP_TOKEN'
2218
2246
  | 'MFA_ENROLLMENT_REQUIRED'
2219
2247
  | 'OTP_REQUIRED'
@@ -3772,6 +3800,7 @@ export type IdpOptionsOutputResolvers<
3772
3800
  > = {
3773
3801
  assumeEmailVerified?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>
3774
3802
  autoSignUp?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>
3803
+ disableLocalAuthentication?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>
3775
3804
  exclusive?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>
3776
3805
  initReturnsConfig?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>
3777
3806
  requireVerifiedEmail?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>
@@ -566,6 +566,11 @@ const schema: DocumentNode = gql`
566
566
  MFA_ENROLLMENT_REQUIRED
567
567
  EMAIL_NOT_VERIFIED
568
568
  RATE_LIMIT_EXCEEDED
569
+ """
570
+ The person is linked to an identity provider with
571
+ \`disableLocalAuthentication\`, so password sign-in is not available to them.
572
+ """
573
+ IDP_REQUIRED
569
574
  }
570
575
 
571
576
  type SignInResult implements CommonSignInResult {
@@ -867,6 +872,11 @@ const schema: DocumentNode = gql`
867
872
  NOT_A_PERSON
868
873
  """ The person is not allowed to disconnect their last remaining sign-in method. """
869
874
  LAST_AUTH_METHOD
875
+ """
876
+ The provider has \`disableLocalAuthentication\`, so this connection is the only way
877
+ the person may sign in and cannot be removed by them — not even while the provider is disabled.
878
+ """
879
+ IDP_REQUIRED
870
880
  }
871
881
 
872
882
  type IdentityProvider {
@@ -893,6 +903,13 @@ const schema: DocumentNode = gql`
893
903
  Defaults to false.
894
904
  """
895
905
  assumeEmailVerified: Boolean!
906
+ """
907
+ When true, a person linked to this provider may authenticate ONLY through it:
908
+ password sign-in and passwordless sign-in are refused and no password-reset
909
+ mail is sent. Disabling the provider lifts the restriction, which is the
910
+ break-glass for an IdP outage. Defaults to false.
911
+ """
912
+ disableLocalAuthentication: Boolean!
896
913
  }
897
914
 
898
915
  input IDPOptions {
@@ -901,6 +918,7 @@ const schema: DocumentNode = gql`
901
918
  initReturnsConfig: Boolean
902
919
  requireVerifiedEmail: Boolean
903
920
  assumeEmailVerified: Boolean
921
+ disableLocalAuthentication: Boolean
904
922
  }
905
923
 
906
924
  # === passwordless sign in ===
@@ -922,6 +940,11 @@ const schema: DocumentNode = gql`
922
940
  PASSWORDLESS_DISABLED
923
941
  INVALID_CAPTCHA
924
942
  RATE_LIMIT_EXCEEDED
943
+ """
944
+ The person is linked to an identity provider with
945
+ \`disableLocalAuthentication\`, so passwordless sign-in is not available to them.
946
+ """
947
+ IDP_REQUIRED
925
948
  }
926
949
 
927
950
  type InitSignInPasswordlessResult {
@@ -959,6 +982,11 @@ const schema: DocumentNode = gql`
959
982
  OTP_REQUIRED
960
983
  INVALID_OTP_TOKEN
961
984
  MFA_ENROLLMENT_REQUIRED
985
+ """
986
+ The person became linked to an identity provider with
987
+ \`disableLocalAuthentication\` after this magic link was issued.
988
+ """
989
+ IDP_REQUIRED
962
990
  }
963
991
 
964
992
  type SignInPasswordlessResult implements CommonSignInResult {
@@ -21,3 +21,13 @@ export const signInPasswordlessMutation = (
21
21
  }`,
22
22
  variables,
23
23
  })
24
+
25
+ export const initSignInPasswordlessMutation = (variables: { email: string }): GraphQLTestQuery => ({
26
+ query: GQL`mutation($email: String!) {
27
+ initSignInPasswordless(email: $email) {
28
+ ok
29
+ error { code }
30
+ }
31
+ }`,
32
+ variables,
33
+ })
@@ -72,7 +72,7 @@ test('addIDP accepts a claimMapping whose membership role IS defined in the proj
72
72
  ...sqlTransaction(
73
73
  {
74
74
  sql:
75
- 'select "id", "slug", "type", "configuration", "disabled_at" as "disabledAt", "auto_sign_up" as "autoSignUp", "exclusive", "init_returns_config" as "initReturnsConfig", "require_verified_email" as "requireVerifiedEmail", "assume_email_verified" as "assumeEmailVerified" from "tenant"."identity_provider" where "slug" = ?',
75
+ 'select "id", "slug", "type", "configuration", "disabled_at" as "disabledAt", "auto_sign_up" as "autoSignUp", "exclusive", "init_returns_config" as "initReturnsConfig", "require_verified_email" as "requireVerifiedEmail", "assume_email_verified" as "assumeEmailVerified", "disable_local_authentication" as "disableLocalAuthentication" from "tenant"."identity_provider" where "slug" = ?',
76
76
  parameters: ['oidc'],
77
77
  response: { rows: [{ id: '123', slug: 'oidc' }] },
78
78
  },
@@ -227,7 +227,7 @@ test('addIDP accepts a condition variable bounded by an allow allowlist plus a v
227
227
  ...sqlTransaction(
228
228
  {
229
229
  sql:
230
- 'select "id", "slug", "type", "configuration", "disabled_at" as "disabledAt", "auto_sign_up" as "autoSignUp", "exclusive", "init_returns_config" as "initReturnsConfig", "require_verified_email" as "requireVerifiedEmail", "assume_email_verified" as "assumeEmailVerified" from "tenant"."identity_provider" where "slug" = ?',
230
+ 'select "id", "slug", "type", "configuration", "disabled_at" as "disabledAt", "auto_sign_up" as "autoSignUp", "exclusive", "init_returns_config" as "initReturnsConfig", "require_verified_email" as "requireVerifiedEmail", "assume_email_verified" as "assumeEmailVerified", "disable_local_authentication" as "disableLocalAuthentication" from "tenant"."identity_provider" where "slug" = ?',
231
231
  parameters: ['oidc'],
232
232
  response: { rows: [{ id: '123', slug: 'oidc' }] },
233
233
  },
@@ -274,7 +274,7 @@ test('addIDP accepts a condition variable whose constant value IS a valid condit
274
274
  ...sqlTransaction(
275
275
  {
276
276
  sql:
277
- 'select "id", "slug", "type", "configuration", "disabled_at" as "disabledAt", "auto_sign_up" as "autoSignUp", "exclusive", "init_returns_config" as "initReturnsConfig", "require_verified_email" as "requireVerifiedEmail", "assume_email_verified" as "assumeEmailVerified" from "tenant"."identity_provider" where "slug" = ?',
277
+ 'select "id", "slug", "type", "configuration", "disabled_at" as "disabledAt", "auto_sign_up" as "autoSignUp", "exclusive", "init_returns_config" as "initReturnsConfig", "require_verified_email" as "requireVerifiedEmail", "assume_email_verified" as "assumeEmailVerified", "disable_local_authentication" as "disableLocalAuthentication" from "tenant"."identity_provider" where "slug" = ?',
278
278
  parameters: ['oidc'],
279
279
  response: { rows: [{ id: '123', slug: 'oidc' }] },
280
280
  },
@@ -305,7 +305,7 @@ test('updateIDP runs the same claimMapping validation (rejects a merged-in grant
305
305
  }
306
306
  const selectIdp = {
307
307
  sql:
308
- 'select "id", "slug", "type", "configuration", "disabled_at" as "disabledAt", "auto_sign_up" as "autoSignUp", "exclusive", "init_returns_config" as "initReturnsConfig", "require_verified_email" as "requireVerifiedEmail", "assume_email_verified" as "assumeEmailVerified" from "tenant"."identity_provider" where "slug" = ?',
308
+ 'select "id", "slug", "type", "configuration", "disabled_at" as "disabledAt", "auto_sign_up" as "autoSignUp", "exclusive", "init_returns_config" as "initReturnsConfig", "require_verified_email" as "requireVerifiedEmail", "assume_email_verified" as "assumeEmailVerified", "disable_local_authentication" as "disableLocalAuthentication" from "tenant"."identity_provider" where "slug" = ?',
309
309
  parameters: ['mock'],
310
310
  response: { rows: [idpRow] },
311
311
  }