@open-mercato/enterprise 0.4.6-develop-15c18897fc → 0.4.6-develop-34aa847ce6

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 (195) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/index.js.map +2 -2
  3. package/dist/modules/sso/acl.js +11 -0
  4. package/dist/modules/sso/acl.js.map +7 -0
  5. package/dist/modules/sso/api/admin-context.js +27 -0
  6. package/dist/modules/sso/api/admin-context.js.map +7 -0
  7. package/dist/modules/sso/api/callback/oidc/route.js +103 -0
  8. package/dist/modules/sso/api/callback/oidc/route.js.map +7 -0
  9. package/dist/modules/sso/api/config/[id]/activate/route.js +49 -0
  10. package/dist/modules/sso/api/config/[id]/activate/route.js.map +7 -0
  11. package/dist/modules/sso/api/config/[id]/domains/route.js +96 -0
  12. package/dist/modules/sso/api/config/[id]/domains/route.js.map +7 -0
  13. package/dist/modules/sso/api/config/[id]/route.js +103 -0
  14. package/dist/modules/sso/api/config/[id]/route.js.map +7 -0
  15. package/dist/modules/sso/api/config/[id]/test/route.js +41 -0
  16. package/dist/modules/sso/api/config/[id]/test/route.js.map +7 -0
  17. package/dist/modules/sso/api/config/route.js +83 -0
  18. package/dist/modules/sso/api/config/route.js.map +7 -0
  19. package/dist/modules/sso/api/error-handler.js +28 -0
  20. package/dist/modules/sso/api/error-handler.js.map +7 -0
  21. package/dist/modules/sso/api/hrd/route.js +52 -0
  22. package/dist/modules/sso/api/hrd/route.js.map +7 -0
  23. package/dist/modules/sso/api/initiate/route.js +66 -0
  24. package/dist/modules/sso/api/initiate/route.js.map +7 -0
  25. package/dist/modules/sso/api/scim/context.js +68 -0
  26. package/dist/modules/sso/api/scim/context.js.map +7 -0
  27. package/dist/modules/sso/api/scim/logs/route.js +65 -0
  28. package/dist/modules/sso/api/scim/logs/route.js.map +7 -0
  29. package/dist/modules/sso/api/scim/tokens/[id]/route.js +42 -0
  30. package/dist/modules/sso/api/scim/tokens/[id]/route.js.map +7 -0
  31. package/dist/modules/sso/api/scim/tokens/route.js +83 -0
  32. package/dist/modules/sso/api/scim/tokens/route.js.map +7 -0
  33. package/dist/modules/sso/api/scim/v2/ServiceProviderConfig/route.js +42 -0
  34. package/dist/modules/sso/api/scim/v2/ServiceProviderConfig/route.js.map +7 -0
  35. package/dist/modules/sso/api/scim/v2/Users/[id]/route.js +94 -0
  36. package/dist/modules/sso/api/scim/v2/Users/[id]/route.js.map +7 -0
  37. package/dist/modules/sso/api/scim/v2/Users/route.js +86 -0
  38. package/dist/modules/sso/api/scim/v2/Users/route.js.map +7 -0
  39. package/dist/modules/sso/backend/page.js +173 -0
  40. package/dist/modules/sso/backend/page.js.map +7 -0
  41. package/dist/modules/sso/backend/page.meta.js +31 -0
  42. package/dist/modules/sso/backend/page.meta.js.map +7 -0
  43. package/dist/modules/sso/backend/sso/config/[id]/page.js +749 -0
  44. package/dist/modules/sso/backend/sso/config/[id]/page.js.map +7 -0
  45. package/dist/modules/sso/backend/sso/config/[id]/page.meta.js +19 -0
  46. package/dist/modules/sso/backend/sso/config/[id]/page.meta.js.map +7 -0
  47. package/dist/modules/sso/backend/sso/config/new/page.js +381 -0
  48. package/dist/modules/sso/backend/sso/config/new/page.js.map +7 -0
  49. package/dist/modules/sso/backend/sso/config/new/page.meta.js +19 -0
  50. package/dist/modules/sso/backend/sso/config/new/page.meta.js.map +7 -0
  51. package/dist/modules/sso/data/entities.js +299 -0
  52. package/dist/modules/sso/data/entities.js.map +7 -0
  53. package/dist/modules/sso/data/validators.js +114 -0
  54. package/dist/modules/sso/data/validators.js.map +7 -0
  55. package/dist/modules/sso/di.js +26 -0
  56. package/dist/modules/sso/di.js.map +7 -0
  57. package/dist/modules/sso/events.js +24 -0
  58. package/dist/modules/sso/events.js.map +7 -0
  59. package/dist/modules/sso/i18n/de.json +146 -0
  60. package/dist/modules/sso/i18n/en.json +146 -0
  61. package/dist/modules/sso/i18n/es.json +146 -0
  62. package/dist/modules/sso/i18n/pl.json +146 -0
  63. package/dist/modules/sso/index.js +11 -0
  64. package/dist/modules/sso/index.js.map +7 -0
  65. package/dist/modules/sso/lib/domains.js +30 -0
  66. package/dist/modules/sso/lib/domains.js.map +7 -0
  67. package/dist/modules/sso/lib/oidc-provider.js +140 -0
  68. package/dist/modules/sso/lib/oidc-provider.js.map +7 -0
  69. package/dist/modules/sso/lib/registry.js +15 -0
  70. package/dist/modules/sso/lib/registry.js.map +7 -0
  71. package/dist/modules/sso/lib/scim-filter.js +43 -0
  72. package/dist/modules/sso/lib/scim-filter.js.map +7 -0
  73. package/dist/modules/sso/lib/scim-mapper.js +49 -0
  74. package/dist/modules/sso/lib/scim-mapper.js.map +7 -0
  75. package/dist/modules/sso/lib/scim-patch.js +63 -0
  76. package/dist/modules/sso/lib/scim-patch.js.map +7 -0
  77. package/dist/modules/sso/lib/scim-response.js +34 -0
  78. package/dist/modules/sso/lib/scim-response.js.map +7 -0
  79. package/dist/modules/sso/lib/scim-utils.js +9 -0
  80. package/dist/modules/sso/lib/scim-utils.js.map +7 -0
  81. package/dist/modules/sso/lib/state-cookie.js +67 -0
  82. package/dist/modules/sso/lib/state-cookie.js.map +7 -0
  83. package/dist/modules/sso/lib/types.js +1 -0
  84. package/dist/modules/sso/lib/types.js.map +7 -0
  85. package/dist/modules/sso/migrations/Migration20260219000000_sso.js +20 -0
  86. package/dist/modules/sso/migrations/Migration20260219000000_sso.js.map +7 -0
  87. package/dist/modules/sso/migrations/Migration20260222000000_sso_add_name.js +13 -0
  88. package/dist/modules/sso/migrations/Migration20260222000000_sso_add_name.js.map +7 -0
  89. package/dist/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.js +15 -0
  90. package/dist/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.js.map +7 -0
  91. package/dist/modules/sso/migrations/Migration20260223000000_scim_tables.js +22 -0
  92. package/dist/modules/sso/migrations/Migration20260223000000_scim_tables.js.map +7 -0
  93. package/dist/modules/sso/migrations/Migration20260224000000_sso_external_id.js +15 -0
  94. package/dist/modules/sso/migrations/Migration20260224000000_sso_external_id.js.map +7 -0
  95. package/dist/modules/sso/migrations/Migration20260224100000_sso_role_grants.js +17 -0
  96. package/dist/modules/sso/migrations/Migration20260224100000_sso_role_grants.js.map +7 -0
  97. package/dist/modules/sso/migrations/Migration20260224200000_drop_default_role_id.js +13 -0
  98. package/dist/modules/sso/migrations/Migration20260224200000_drop_default_role_id.js.map +7 -0
  99. package/dist/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.js +23 -0
  100. package/dist/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.js.map +7 -0
  101. package/dist/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.js +14 -0
  102. package/dist/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.js.map +7 -0
  103. package/dist/modules/sso/services/accountLinkingService.js +298 -0
  104. package/dist/modules/sso/services/accountLinkingService.js.map +7 -0
  105. package/dist/modules/sso/services/hrdService.js +18 -0
  106. package/dist/modules/sso/services/hrdService.js.map +7 -0
  107. package/dist/modules/sso/services/scimService.js +372 -0
  108. package/dist/modules/sso/services/scimService.js.map +7 -0
  109. package/dist/modules/sso/services/scimTokenService.js +94 -0
  110. package/dist/modules/sso/services/scimTokenService.js.map +7 -0
  111. package/dist/modules/sso/services/ssoConfigService.js +254 -0
  112. package/dist/modules/sso/services/ssoConfigService.js.map +7 -0
  113. package/dist/modules/sso/services/ssoService.js +125 -0
  114. package/dist/modules/sso/services/ssoService.js.map +7 -0
  115. package/dist/modules/sso/setup.js +47 -0
  116. package/dist/modules/sso/setup.js.map +7 -0
  117. package/dist/modules/sso/subscribers/user-deleted-cleanup.js +21 -0
  118. package/dist/modules/sso/subscribers/user-deleted-cleanup.js.map +7 -0
  119. package/dist/modules/sso/widgets/injection/login-sso/widget.client.js +106 -0
  120. package/dist/modules/sso/widgets/injection/login-sso/widget.client.js.map +7 -0
  121. package/dist/modules/sso/widgets/injection/login-sso/widget.js +16 -0
  122. package/dist/modules/sso/widgets/injection/login-sso/widget.js.map +7 -0
  123. package/dist/modules/sso/widgets/injection-table.js +14 -0
  124. package/dist/modules/sso/widgets/injection-table.js.map +7 -0
  125. package/package.json +5 -4
  126. package/src/index.ts +1 -1
  127. package/src/modules/sso/acl.ts +7 -0
  128. package/src/modules/sso/api/admin-context.ts +36 -0
  129. package/src/modules/sso/api/callback/oidc/route.ts +115 -0
  130. package/src/modules/sso/api/config/[id]/activate/route.ts +53 -0
  131. package/src/modules/sso/api/config/[id]/domains/route.ts +107 -0
  132. package/src/modules/sso/api/config/[id]/route.ts +114 -0
  133. package/src/modules/sso/api/config/[id]/test/route.ts +44 -0
  134. package/src/modules/sso/api/config/route.ts +88 -0
  135. package/src/modules/sso/api/error-handler.ts +36 -0
  136. package/src/modules/sso/api/hrd/route.ts +55 -0
  137. package/src/modules/sso/api/initiate/route.ts +70 -0
  138. package/src/modules/sso/api/scim/context.ts +85 -0
  139. package/src/modules/sso/api/scim/logs/route.ts +69 -0
  140. package/src/modules/sso/api/scim/tokens/[id]/route.ts +45 -0
  141. package/src/modules/sso/api/scim/tokens/route.ts +89 -0
  142. package/src/modules/sso/api/scim/v2/ServiceProviderConfig/route.ts +40 -0
  143. package/src/modules/sso/api/scim/v2/Users/[id]/route.ts +103 -0
  144. package/src/modules/sso/api/scim/v2/Users/route.ts +94 -0
  145. package/src/modules/sso/backend/page.meta.ts +29 -0
  146. package/src/modules/sso/backend/page.tsx +232 -0
  147. package/src/modules/sso/backend/sso/config/[id]/page.meta.ts +15 -0
  148. package/src/modules/sso/backend/sso/config/[id]/page.tsx +1024 -0
  149. package/src/modules/sso/backend/sso/config/new/page.meta.ts +15 -0
  150. package/src/modules/sso/backend/sso/config/new/page.tsx +463 -0
  151. package/src/modules/sso/data/entities.ts +240 -0
  152. package/src/modules/sso/data/validators.ts +140 -0
  153. package/src/modules/sso/di.ts +25 -0
  154. package/src/modules/sso/docs/entra-id-setup.md +281 -0
  155. package/src/modules/sso/docs/google-workspace-setup.md +174 -0
  156. package/src/modules/sso/docs/sso-overview.md +218 -0
  157. package/src/modules/sso/docs/sso-security-audit-2026-02-27.md +118 -0
  158. package/src/modules/sso/docs/zitadel-setup.md +195 -0
  159. package/src/modules/sso/events.ts +21 -0
  160. package/src/modules/sso/i18n/de.json +146 -0
  161. package/src/modules/sso/i18n/en.json +146 -0
  162. package/src/modules/sso/i18n/es.json +146 -0
  163. package/src/modules/sso/i18n/pl.json +146 -0
  164. package/src/modules/sso/index.ts +7 -0
  165. package/src/modules/sso/lib/domains.ts +31 -0
  166. package/src/modules/sso/lib/oidc-provider.ts +196 -0
  167. package/src/modules/sso/lib/registry.ts +13 -0
  168. package/src/modules/sso/lib/scim-filter.ts +62 -0
  169. package/src/modules/sso/lib/scim-mapper.ts +88 -0
  170. package/src/modules/sso/lib/scim-patch.ts +88 -0
  171. package/src/modules/sso/lib/scim-response.ts +40 -0
  172. package/src/modules/sso/lib/scim-utils.ts +5 -0
  173. package/src/modules/sso/lib/state-cookie.ts +79 -0
  174. package/src/modules/sso/lib/types.ts +50 -0
  175. package/src/modules/sso/migrations/.snapshot-open-mercato.json +912 -0
  176. package/src/modules/sso/migrations/Migration20260219000000_sso.ts +21 -0
  177. package/src/modules/sso/migrations/Migration20260222000000_sso_add_name.ts +13 -0
  178. package/src/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.ts +15 -0
  179. package/src/modules/sso/migrations/Migration20260223000000_scim_tables.ts +24 -0
  180. package/src/modules/sso/migrations/Migration20260224000000_sso_external_id.ts +15 -0
  181. package/src/modules/sso/migrations/Migration20260224100000_sso_role_grants.ts +18 -0
  182. package/src/modules/sso/migrations/Migration20260224200000_drop_default_role_id.ts +13 -0
  183. package/src/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.ts +25 -0
  184. package/src/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.ts +14 -0
  185. package/src/modules/sso/services/accountLinkingService.ts +386 -0
  186. package/src/modules/sso/services/hrdService.ts +22 -0
  187. package/src/modules/sso/services/scimService.ts +461 -0
  188. package/src/modules/sso/services/scimTokenService.ts +136 -0
  189. package/src/modules/sso/services/ssoConfigService.ts +337 -0
  190. package/src/modules/sso/services/ssoService.ts +167 -0
  191. package/src/modules/sso/setup.ts +56 -0
  192. package/src/modules/sso/subscribers/user-deleted-cleanup.ts +33 -0
  193. package/src/modules/sso/widgets/injection/login-sso/widget.client.tsx +130 -0
  194. package/src/modules/sso/widgets/injection/login-sso/widget.ts +16 -0
  195. package/src/modules/sso/widgets/injection-table.ts +12 -0
@@ -0,0 +1,240 @@
1
+ import { Entity, PrimaryKey, Property, Unique, Index } from '@mikro-orm/core'
2
+
3
+ @Entity({ tableName: 'sso_configs' })
4
+ // Unique index on organization_id (partial: WHERE deleted_at IS NULL) — managed by migration
5
+ export class SsoConfig {
6
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
7
+ id!: string
8
+
9
+ @Property({ name: 'tenant_id', type: 'uuid', nullable: true })
10
+ tenantId?: string | null
11
+
12
+ @Property({ name: 'organization_id', type: 'uuid' })
13
+ organizationId!: string
14
+
15
+ @Property({ type: 'text', nullable: true })
16
+ name?: string | null
17
+
18
+ @Property({ type: 'text' })
19
+ protocol!: string
20
+
21
+ @Property({ type: 'text', nullable: true })
22
+ issuer?: string | null
23
+
24
+ @Property({ name: 'client_id', type: 'text', nullable: true })
25
+ clientId?: string | null
26
+
27
+ @Property({ name: 'client_secret_enc', type: 'text', nullable: true })
28
+ clientSecretEnc?: string | null
29
+
30
+ @Property({ name: 'allowed_domains', type: 'jsonb', default: '[]' })
31
+ allowedDomains: string[] = []
32
+
33
+ @Property({ name: 'jit_enabled', type: 'boolean', default: true })
34
+ jitEnabled: boolean = true
35
+
36
+ @Property({ name: 'auto_link_by_email', type: 'boolean', default: true })
37
+ autoLinkByEmail: boolean = true
38
+
39
+ @Property({ name: 'is_active', type: 'boolean', default: false })
40
+ isActive: boolean = false
41
+
42
+ @Property({ name: 'sso_required', type: 'boolean', default: false })
43
+ ssoRequired: boolean = false
44
+
45
+ @Property({ name: 'app_role_mappings', type: 'jsonb', default: '{}' })
46
+ appRoleMappings: Record<string, string> = {}
47
+
48
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
49
+ createdAt: Date = new Date()
50
+
51
+ @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })
52
+ updatedAt: Date = new Date()
53
+
54
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
55
+ deletedAt?: Date | null
56
+ }
57
+
58
+ @Entity({ tableName: 'sso_identities' })
59
+ // Unique indexes (partial: WHERE deleted_at IS NULL) — managed by migration
60
+ export class SsoIdentity {
61
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
62
+ id!: string
63
+
64
+ @Property({ name: 'tenant_id', type: 'uuid', nullable: true })
65
+ tenantId?: string | null
66
+
67
+ @Property({ name: 'organization_id', type: 'uuid' })
68
+ organizationId!: string
69
+
70
+ @Property({ name: 'sso_config_id', type: 'uuid' })
71
+ @Index({ name: 'sso_identities_config_id_idx' })
72
+ ssoConfigId!: string
73
+
74
+ @Property({ name: 'user_id', type: 'uuid' })
75
+ @Index({ name: 'sso_identities_user_id_idx' })
76
+ userId!: string
77
+
78
+ @Property({ name: 'idp_subject', type: 'text' })
79
+ idpSubject!: string
80
+
81
+ @Property({ name: 'idp_email', type: 'text' })
82
+ idpEmail!: string
83
+
84
+ @Property({ name: 'idp_name', type: 'text', nullable: true })
85
+ idpName?: string | null
86
+
87
+ @Property({ name: 'idp_groups', type: 'jsonb', default: '[]' })
88
+ idpGroups: string[] = []
89
+
90
+ @Property({ name: 'external_id', type: 'text', nullable: true })
91
+ externalId?: string | null
92
+
93
+ @Property({ name: 'provisioning_method', type: 'text' })
94
+ provisioningMethod!: string
95
+
96
+ @Property({ name: 'first_login_at', type: Date, nullable: true })
97
+ firstLoginAt?: Date | null
98
+
99
+ @Property({ name: 'last_login_at', type: Date, nullable: true })
100
+ lastLoginAt?: Date | null
101
+
102
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
103
+ createdAt: Date = new Date()
104
+
105
+ @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })
106
+ updatedAt: Date = new Date()
107
+
108
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
109
+ deletedAt?: Date | null
110
+ }
111
+
112
+ @Entity({ tableName: 'scim_tokens' })
113
+ @Index({ name: 'scim_tokens_token_prefix_idx', properties: ['tokenPrefix'] })
114
+ export class ScimToken {
115
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
116
+ id!: string
117
+
118
+ @Property({ name: 'tenant_id', type: 'uuid', nullable: true })
119
+ tenantId?: string | null
120
+
121
+ @Property({ name: 'organization_id', type: 'uuid' })
122
+ organizationId!: string
123
+
124
+ @Property({ name: 'sso_config_id', type: 'uuid' })
125
+ @Index({ name: 'scim_tokens_sso_config_id_idx' })
126
+ ssoConfigId!: string
127
+
128
+ @Property({ type: 'text' })
129
+ name!: string
130
+
131
+ @Property({ name: 'token_hash', type: 'text' })
132
+ tokenHash!: string
133
+
134
+ @Property({ name: 'token_prefix', type: 'text' })
135
+ tokenPrefix!: string
136
+
137
+ @Property({ name: 'is_active', type: 'boolean', default: true })
138
+ isActive: boolean = true
139
+
140
+ @Property({ name: 'created_by', type: 'uuid', nullable: true })
141
+ createdBy?: string | null
142
+
143
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
144
+ createdAt: Date = new Date()
145
+
146
+ @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })
147
+ updatedAt: Date = new Date()
148
+ }
149
+
150
+ @Entity({ tableName: 'sso_user_deactivations' })
151
+ @Unique({ properties: ['userId', 'ssoConfigId'], name: 'sso_user_deactivations_user_config_unique' })
152
+ export class SsoUserDeactivation {
153
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
154
+ id!: string
155
+
156
+ @Property({ name: 'tenant_id', type: 'uuid', nullable: true })
157
+ tenantId?: string | null
158
+
159
+ @Property({ name: 'organization_id', type: 'uuid' })
160
+ organizationId!: string
161
+
162
+ @Property({ name: 'user_id', type: 'uuid' })
163
+ @Index({ name: 'sso_user_deactivations_user_id_idx' })
164
+ userId!: string
165
+
166
+ @Property({ name: 'sso_config_id', type: 'uuid' })
167
+ ssoConfigId!: string
168
+
169
+ @Property({ name: 'deactivated_at', type: Date })
170
+ deactivatedAt: Date = new Date()
171
+
172
+ @Property({ name: 'reactivated_at', type: Date, nullable: true })
173
+ reactivatedAt?: Date | null
174
+
175
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
176
+ createdAt: Date = new Date()
177
+ }
178
+
179
+ @Entity({ tableName: 'scim_provisioning_log' })
180
+ @Index({ name: 'scim_provisioning_log_config_created_idx', properties: ['ssoConfigId', 'createdAt'] })
181
+ export class ScimProvisioningLog {
182
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
183
+ id!: string
184
+
185
+ @Property({ name: 'tenant_id', type: 'uuid', nullable: true })
186
+ tenantId?: string | null
187
+
188
+ @Property({ name: 'organization_id', type: 'uuid' })
189
+ organizationId!: string
190
+
191
+ @Property({ name: 'sso_config_id', type: 'uuid' })
192
+ ssoConfigId!: string
193
+
194
+ @Property({ type: 'text' })
195
+ operation!: string
196
+
197
+ @Property({ name: 'resource_type', type: 'text' })
198
+ resourceType!: string
199
+
200
+ @Property({ name: 'resource_id', type: 'uuid', nullable: true })
201
+ resourceId?: string | null
202
+
203
+ @Property({ name: 'scim_external_id', type: 'text', nullable: true })
204
+ scimExternalId?: string | null
205
+
206
+ @Property({ name: 'response_status', type: 'integer' })
207
+ responseStatus!: number
208
+
209
+ @Property({ name: 'error_message', type: 'text', nullable: true })
210
+ errorMessage?: string | null
211
+
212
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
213
+ createdAt: Date = new Date()
214
+ }
215
+
216
+ @Entity({ tableName: 'sso_role_grants' })
217
+ @Unique({ properties: ['userId', 'roleId', 'ssoConfigId'], name: 'sso_role_grants_user_role_config_unique' })
218
+ export class SsoRoleGrant {
219
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
220
+ id!: string
221
+
222
+ @Property({ name: 'tenant_id', type: 'uuid', nullable: true })
223
+ tenantId?: string | null
224
+
225
+ @Property({ name: 'organization_id', type: 'uuid' })
226
+ organizationId!: string
227
+
228
+ @Property({ name: 'user_id', type: 'uuid' })
229
+ @Index({ name: 'sso_role_grants_user_id_idx' })
230
+ userId!: string
231
+
232
+ @Property({ name: 'role_id', type: 'uuid' })
233
+ roleId!: string
234
+
235
+ @Property({ name: 'sso_config_id', type: 'uuid' })
236
+ ssoConfigId!: string
237
+
238
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
239
+ createdAt: Date = new Date()
240
+ }
@@ -0,0 +1,140 @@
1
+ import { z } from 'zod'
2
+ import { validateDomain } from '../lib/domains'
3
+
4
+ const uuid = () => z.string().uuid()
5
+
6
+ const domainString = () =>
7
+ z.string().trim().min(1).max(253).refine(
8
+ (val) => validateDomain(val).valid,
9
+ { message: 'Invalid domain format — only valid DNS hostnames with at least one dot are accepted' },
10
+ )
11
+
12
+ // --- SSO Config schema (for internal use / seeding) ---
13
+
14
+ export const ssoConfigCreateSchema = z.object({
15
+ organizationId: uuid(),
16
+ tenantId: uuid().optional(),
17
+ protocol: z.enum(['oidc', 'saml']),
18
+ issuer: z.string().url().optional(),
19
+ clientId: z.string().min(1).optional(),
20
+ clientSecret: z.string().min(1).optional(),
21
+ allowedDomains: z.array(domainString()).default([]),
22
+ jitEnabled: z.boolean().default(true),
23
+ autoLinkByEmail: z.boolean().default(true),
24
+ isActive: z.boolean().default(false),
25
+ ssoRequired: z.boolean().default(false),
26
+ appRoleMappings: z.record(z.string().min(1).max(255), z.string().min(1).max(255)).default({}),
27
+ })
28
+
29
+ export const ssoConfigUpdateSchema = z
30
+ .object({
31
+ id: uuid(),
32
+ })
33
+ .merge(ssoConfigCreateSchema.partial().omit({ organizationId: true, tenantId: true }))
34
+
35
+ // --- API request schemas ---
36
+
37
+ export const hrdRequestSchema = z.object({
38
+ email: z.string().email(),
39
+ })
40
+
41
+ export const ssoInitiateSchema = z.object({
42
+ configId: uuid(),
43
+ returnUrl: z.string().max(2048).refine(
44
+ (val) => val.startsWith('/') && !val.startsWith('//'),
45
+ { message: 'returnUrl must be a relative path starting with / and must not start with //' },
46
+ ).optional(),
47
+ })
48
+
49
+ export const oidcCallbackSchema = z.object({
50
+ code: z.string().min(1),
51
+ state: z.string().min(1),
52
+ })
53
+
54
+ // --- Admin API schemas ---
55
+
56
+ export const ssoConfigAdminCreateSchema = z.object({
57
+ name: z.string().min(1).max(255),
58
+ organizationId: uuid().optional(),
59
+ tenantId: uuid().optional(),
60
+ protocol: z.enum(['oidc', 'saml']),
61
+ issuer: z.string().url(),
62
+ clientId: z.string().min(1),
63
+ clientSecret: z.string().min(1),
64
+ allowedDomains: z.array(domainString()).default([]),
65
+ jitEnabled: z.boolean().default(true),
66
+ autoLinkByEmail: z.boolean().default(true),
67
+ appRoleMappings: z.record(z.string().min(1).max(255), z.string().min(1).max(255)).default({}),
68
+ })
69
+
70
+ export const ssoConfigAdminUpdateSchema = z.object({
71
+ name: z.string().min(1).max(255).optional(),
72
+ protocol: z.enum(['oidc', 'saml']).optional(),
73
+ issuer: z.string().url().optional(),
74
+ clientId: z.string().min(1).optional(),
75
+ clientSecret: z.string().min(1).optional(),
76
+ jitEnabled: z.boolean().optional(),
77
+ autoLinkByEmail: z.boolean().optional(),
78
+ appRoleMappings: z.record(z.string().min(1).max(255), z.string().min(1).max(255)).optional(),
79
+ })
80
+
81
+ export const ssoConfigListQuerySchema = z.object({
82
+ page: z.coerce.number().min(1).default(1),
83
+ pageSize: z.coerce.number().min(1).max(100).default(50),
84
+ search: z.string().optional(),
85
+ organizationId: uuid().optional(),
86
+ tenantId: uuid().optional(),
87
+ })
88
+
89
+ export const ssoDomainAddSchema = z.object({
90
+ domain: domainString(),
91
+ })
92
+
93
+ export const ssoActivateSchema = z.object({
94
+ active: z.boolean(),
95
+ })
96
+
97
+ // --- SCIM User payload schema ---
98
+
99
+ export const scimUserPayloadSchema = z.object({
100
+ schemas: z.array(z.string()).optional(),
101
+ userName: z.string().min(1).max(255),
102
+ externalId: z.string().max(255).optional(),
103
+ displayName: z.string().max(255).optional(),
104
+ active: z.union([z.boolean(), z.string()]).optional(),
105
+ name: z.object({
106
+ givenName: z.string().max(255).optional(),
107
+ familyName: z.string().max(255).optional(),
108
+ formatted: z.string().max(512).optional(),
109
+ }).optional(),
110
+ emails: z.array(z.object({
111
+ value: z.string().email(),
112
+ primary: z.boolean().optional(),
113
+ type: z.string().optional(),
114
+ })).optional(),
115
+ }).passthrough()
116
+
117
+ // --- SCIM Token schemas ---
118
+
119
+ export const createScimTokenSchema = z.object({
120
+ ssoConfigId: uuid(),
121
+ name: z.string().min(1).max(100),
122
+ })
123
+
124
+ export const scimTokenListSchema = z.object({
125
+ ssoConfigId: uuid(),
126
+ })
127
+
128
+ // --- Type exports ---
129
+
130
+ export type SsoConfigCreateInput = z.infer<typeof ssoConfigCreateSchema>
131
+ export type SsoConfigUpdateInput = z.infer<typeof ssoConfigUpdateSchema>
132
+ export type SsoConfigAdminCreateInput = z.infer<typeof ssoConfigAdminCreateSchema>
133
+ export type SsoConfigAdminUpdateInput = z.infer<typeof ssoConfigAdminUpdateSchema>
134
+ export type SsoConfigListQuery = z.infer<typeof ssoConfigListQuerySchema>
135
+ export type HrdRequestInput = z.infer<typeof hrdRequestSchema>
136
+ export type SsoInitiateInput = z.infer<typeof ssoInitiateSchema>
137
+ export type OidcCallbackInput = z.infer<typeof oidcCallbackSchema>
138
+ export type ScimUserPayloadInput = z.infer<typeof scimUserPayloadSchema>
139
+ export type CreateScimTokenInput = z.infer<typeof createScimTokenSchema>
140
+ export type ScimTokenListInput = z.infer<typeof scimTokenListSchema>
@@ -0,0 +1,25 @@
1
+ import { asClass, asValue } from 'awilix'
2
+ import type { AppContainer } from '@open-mercato/shared/lib/di/container'
3
+ import { SsoProviderRegistry } from './lib/registry'
4
+ import { OidcProvider } from './lib/oidc-provider'
5
+ import { SsoService } from './services/ssoService'
6
+ import { AccountLinkingService } from './services/accountLinkingService'
7
+ import { SsoConfigService } from './services/ssoConfigService'
8
+ import { HrdService } from './services/hrdService'
9
+ import { ScimTokenService } from './services/scimTokenService'
10
+ import { ScimService } from './services/scimService'
11
+
12
+ export function register(container: AppContainer) {
13
+ const registry = new SsoProviderRegistry()
14
+ registry.register(new OidcProvider())
15
+
16
+ container.register({
17
+ ssoProviderRegistry: asValue(registry),
18
+ ssoService: asClass(SsoService).scoped(),
19
+ accountLinkingService: asClass(AccountLinkingService).scoped(),
20
+ ssoConfigService: asClass(SsoConfigService).scoped(),
21
+ hrdService: asClass(HrdService).scoped(),
22
+ scimTokenService: asClass(ScimTokenService).scoped(),
23
+ scimService: asClass(ScimService).scoped(),
24
+ })
25
+ }
@@ -0,0 +1,281 @@
1
+ # Microsoft Entra ID Setup Guide for Open Mercato SSO + SCIM
2
+
3
+ This guide walks through setting up Microsoft Entra ID (formerly Azure AD) as the identity provider for both OIDC login and SCIM user provisioning in Open Mercato.
4
+
5
+ **Free tier**: Entra ID Free is included with any Azure subscription — no paid license required for basic OIDC + SCIM.
6
+
7
+ ---
8
+
9
+ ## 1. Create an Azure Account + Entra ID Tenant
10
+
11
+ 1. Go to https://azure.microsoft.com/free and create a free account (or use an existing one)
12
+ 2. Navigate to https://entra.microsoft.com (the Entra admin center)
13
+ 3. You'll have a default tenant — note your **Tenant ID** from **Overview** → **Tenant ID**
14
+
15
+ ## 2. Create Test Users
16
+
17
+ 1. In the Entra admin center, go to **Identity** → **Users** → **All users**
18
+ 2. Click **+ New user** → **Create new user**
19
+ 3. Fill in:
20
+ - **User principal name**: e.g., `testuser@yourtenant.onmicrosoft.com`
21
+ - **Display name**: e.g., `Test User`
22
+ - **First name** / **Last name**
23
+ - **Password**: auto-generate or set manually
24
+ 4. Click **Create**
25
+ 5. Repeat for 2-3 test users
26
+
27
+ ## 3. Register the OIDC Application (SSO Login)
28
+
29
+ 1. In the Entra admin center, go to **Identity** → **Applications** → **App registrations**
30
+ 2. Click **+ New registration**
31
+ 3. Configure:
32
+
33
+ | Field | Value |
34
+ |-------|-------|
35
+ | **Name** | `Open Mercato` |
36
+ | **Supported account types** | `Accounts in this organizational directory only` (Single tenant) |
37
+ | **Redirect URI** | Platform: `Web`, URI: `http://localhost:3000/api/sso/callback/oidc` |
38
+
39
+ 4. Click **Register**
40
+ 5. You'll land on the app's **Overview** page — note:
41
+ - **Application (client) ID** — this is your Client ID
42
+ - **Directory (tenant) ID** — used in the issuer URL
43
+
44
+ ### Create a Client Secret
45
+
46
+ 1. Go to **Certificates & secrets** → **Client secrets** tab
47
+ 2. Click **+ New client secret**
48
+ 3. Description: `Open Mercato Dev`, Expiry: `6 months` (or your preference)
49
+ 4. Click **Add**
50
+ 5. **Copy the secret Value immediately** — it's shown only once
51
+
52
+ ### OIDC Credentials Summary
53
+
54
+ | Credential | Where to find it | Value |
55
+ |------------|-----------------|-------|
56
+ | **Issuer URL** | Computed from Tenant ID | `https://login.microsoftonline.com/{tenant-id}/v2.0` |
57
+ | **Client ID** | App registration → Overview | Copy from portal |
58
+ | **Client Secret** | App registration → Certificates & secrets | Copy the **Value** (not Secret ID) |
59
+ | **Redirect URI** | You configured this | `http://localhost:3000/api/sso/callback/oidc` |
60
+
61
+ ### Configure Token Claims
62
+
63
+ By default, Entra ID v2.0 tokens may not include `email` in the ID token. Fix this:
64
+
65
+ 1. Go to your App registration → **Token configuration**
66
+ 2. Click **+ Add optional claim**
67
+ 3. Token type: **ID**
68
+ 4. Check: `email`, `given_name`, `family_name`
69
+ 5. Click **Add**
70
+ 6. When prompted about Microsoft Graph permissions, check the box and click **Add**
71
+
72
+ ### API Permissions
73
+
74
+ 1. Go to **API permissions**
75
+ 2. Verify these are present (they should be by default):
76
+ - `Microsoft Graph` → `openid` (Delegated)
77
+ - `Microsoft Graph` → `profile` (Delegated)
78
+ - `Microsoft Graph` → `email` (Delegated)
79
+ 3. If any are missing, click **+ Add a permission** → **Microsoft Graph** → **Delegated permissions** → search and add them
80
+ 4. Click **Grant admin consent for [your tenant]** (green checkmark button)
81
+
82
+ ### Assign Users to the Application
83
+
84
+ 1. Go to **Identity** → **Applications** → **Enterprise applications**
85
+ 2. Find and click **Open Mercato**
86
+ 3. Go to **Users and groups** → **+ Add user/group**
87
+ 4. Select your test users (or a group containing them)
88
+ 5. Click **Assign**
89
+
90
+ **Important**: If "Assignment required?" is set to **Yes** (under Properties), only assigned users can log in. Set to **No** for dev if you want all tenant users to access it.
91
+
92
+ ---
93
+
94
+ ## 4. Create the SSO Config in Open Mercato
95
+
96
+ 1. Log into Open Mercato as admin
97
+ 2. Go to **Settings** → **Single Sign-On** → **Create New**
98
+ 3. Select **OIDC** as the protocol
99
+ 4. Enter:
100
+ - **Name**: `Entra ID`
101
+ - **Issuer URL**: `https://login.microsoftonline.com/{your-tenant-id}/v2.0`
102
+ - **Client ID**: (paste from Entra)
103
+ - **Client Secret**: (paste the secret Value from Entra)
104
+ 5. Add allowed email domains (e.g., `yourtenant.onmicrosoft.com`)
105
+ 6. Test the connection (Verify Discovery)
106
+ 7. Activate the config
107
+
108
+ ### Verify OIDC Login
109
+
110
+ 1. Open a private/incognito browser window
111
+ 2. Go to the Open Mercato login page
112
+ 3. Enter an email address belonging to one of your test users (e.g., `testuser@yourtenant.onmicrosoft.com`)
113
+ 4. The HRD check should detect SSO and redirect to Microsoft login
114
+ 5. Authenticate at Microsoft
115
+ 6. You should be redirected back to Open Mercato and logged in
116
+
117
+ ---
118
+
119
+ ## 5. Configure SCIM Provisioning
120
+
121
+ **Prerequisite**: You need a SCIM bearer token from Open Mercato. Generate one via:
122
+ - The admin UI: SSO config → Provisioning tab → Generate Token
123
+ - Or the API: `POST /api/sso/scim/tokens` with the SSO config ID
124
+
125
+ ### Set Up Provisioning in Entra ID
126
+
127
+ 1. Go to **Identity** → **Applications** → **Enterprise applications**
128
+ 2. Find and click **Open Mercato**
129
+ 3. Go to **Provisioning** → click **Get started**
130
+ 4. Set **Provisioning Mode** to **Automatic**
131
+ 5. In **Admin Credentials**:
132
+
133
+ | Field | Value |
134
+ |-------|-------|
135
+ | **Tenant URL** | `http://localhost:3000/api/sso/scim/v2` (dev) or `https://<your-domain>/api/sso/scim/v2` (prod) |
136
+ | **Secret Token** | Paste the SCIM bearer token from Open Mercato |
137
+
138
+ 6. Click **Test Connection** — should show "The supplied credentials are authorized to enable provisioning"
139
+ 7. Click **Save**
140
+
141
+ ### Configure Attribute Mappings
142
+
143
+ 1. Under **Mappings**, click **Provision Microsoft Entra ID Users**
144
+ 2. Verify these mappings exist:
145
+
146
+ | Entra ID Attribute | SCIM Attribute | Notes |
147
+ |--------------------|----------------|-------|
148
+ | `userPrincipalName` | `userName` | Required |
149
+ | `Switch([IsSoftDeleted]...)` | `active` | Required — Entra uses a Switch expression |
150
+ | `givenName` | `name.givenName` | Required |
151
+ | `surname` | `name.familyName` | Required |
152
+ | `mail` | `emails[type eq "work"].value` | Required — user's email |
153
+ | `displayName` | `displayName` | Optional |
154
+ | `objectId` | `externalId` | Required — Entra's unique ID |
155
+
156
+ 3. Keep default mappings — they should work out of the box
157
+ 4. Click **Save**
158
+
159
+ ### Start Provisioning
160
+
161
+ 1. Back on the **Provisioning** page, set **Provisioning Status** to **On**
162
+ 2. Click **Save**
163
+ 3. Entra will run an initial provisioning cycle (may take up to 40 minutes for the first cycle)
164
+ 4. Check **Provisioning logs** for results
165
+
166
+ ### Provisioning Cycle Timing
167
+
168
+ - **Initial cycle**: Processes all users in scope. Can take 20-40 minutes.
169
+ - **Incremental cycles**: Every 40 minutes, processes changes since last cycle.
170
+ - **On-demand provisioning**: Click **Provision on demand** to immediately provision a specific user (useful for testing).
171
+
172
+ ---
173
+
174
+ ## 6. Test the Full Flow
175
+
176
+ ### Test SCIM Provisioning
177
+
178
+ 1. In Entra, go to **Enterprise applications** → **Open Mercato** → **Provisioning**
179
+ 2. Click **Provision on demand**
180
+ 3. Search for a test user and click **Provision**
181
+ 4. **Expected**: Entra sends `POST /Users` to your SCIM endpoint → user appears in Open Mercato
182
+ 5. Check the provisioning log in Open Mercato admin UI
183
+
184
+ ### Test User Update
185
+
186
+ 1. In Entra, go to **Users** → edit a test user's display name
187
+ 2. Wait for the next provisioning cycle (or use Provision on demand)
188
+ 3. **Expected**: Entra sends `PATCH /Users/{id}` → user's name updated in Open Mercato
189
+
190
+ ### Test User Deactivation
191
+
192
+ 1. In Entra, either:
193
+ - **Delete** the user (soft-delete moves to Deleted users)
194
+ - **Block sign-in** for the user (Users → select user → Edit properties → Block sign in: Yes)
195
+ - **Remove** the user from the application assignment
196
+ 2. **Expected**: Entra sends `PATCH /Users/{id}` with `active: false` → user deactivated in Open Mercato, all sessions revoked
197
+
198
+ ### Test OIDC + SCIM Together
199
+
200
+ 1. **Create a new user in Entra** and assign them to the Open Mercato Enterprise app
201
+ 2. **Provision on demand** (or wait for cycle)
202
+ 3. **Verify** the user exists in Open Mercato (pre-provisioned, no login needed)
203
+ 4. **Log in as that user** via OIDC (Open Mercato login → redirect to Microsoft → authenticate → redirect back)
204
+ 5. **Expected**: The SCIM-provisioned account is used (no JIT provisioning, `provisioningMethod` stays `scim`)
205
+ 6. **Block sign-in for the user in Entra**
206
+ 7. **Expected**: SCIM deactivates the user → existing sessions revoked → OIDC login no longer works
207
+
208
+ ---
209
+
210
+ ## Entra ID SCIM Quirks
211
+
212
+ When building the SCIM endpoint, account for these Entra-specific behaviors:
213
+
214
+ | Quirk | Description | How to handle |
215
+ |-------|-------------|---------------|
216
+ | **PascalCase `op` in PATCH** | Entra sends `"op": "Replace"` instead of `"op": "replace"` | Case-insensitive comparison on PATCH operations |
217
+ | **String booleans** | `active` may be sent as `"True"` / `"False"` strings | Parse with `parseBooleanToken` |
218
+ | **Non-standard PATCH paths** | Sometimes sends `emails[type eq "work"].value` in PATCH path | Support bracket-notation in PATCH path parser |
219
+ | **Mixed-case filter operators** | Sends `Eq` instead of `eq` in filters | Case-insensitive filter parsing |
220
+ | **`externalId` mapping** | Maps `objectId` → `externalId` by default | Always store `externalId` from SCIM requests |
221
+ | **Soft delete** | Uses `IsSoftDeleted` Switch expression → `active: false` | Handle as user deactivation |
222
+
223
+ ---
224
+
225
+ ## Troubleshooting
226
+
227
+ ### OIDC login redirects but fails
228
+
229
+ - Verify the Redirect URI in App Registration matches exactly: `http://localhost:3000/api/sso/callback/oidc`
230
+ - Check that the Issuer URL includes the tenant ID: `https://login.microsoftonline.com/{tenant-id}/v2.0`
231
+ - Verify Client ID and Client Secret (the Value, not the Secret ID)
232
+ - Ensure `email` optional claim is added to the ID token
233
+ - Ensure API permissions have admin consent granted
234
+
235
+ ### "AADSTS50011: The redirect URI does not match"
236
+
237
+ The redirect URI in the authorization request doesn't match what's registered. Check:
238
+ - `APP_URL` in `.env` matches what you registered (e.g., `http://localhost:3000`)
239
+ - No trailing slash differences
240
+ - Protocol matches (http vs https)
241
+
242
+ ### Users not provisioning
243
+
244
+ - Check that users are assigned to the Enterprise application
245
+ - Check **Provisioning logs** in Entra for error details
246
+ - Verify the SCIM token is valid and not revoked
247
+ - For local dev, Entra needs to reach your server — use ngrok for SCIM (even though OIDC works with localhost)
248
+
249
+ ### SCIM "Test Connection" fails
250
+
251
+ - For local dev, Entra's provisioning service needs to reach your endpoint over the internet
252
+ - Use ngrok: `ngrok http 3000`
253
+ - Set Tenant URL to: `https://<id>.ngrok-free.app/api/sso/scim/v2`
254
+ - **Note**: OIDC redirect URIs can use `localhost`, but SCIM provisioning requires a publicly reachable URL
255
+
256
+ ### email claim missing from ID token
257
+
258
+ 1. Go to App registration → **Token configuration** → **+ Add optional claim** → ID token → check `email`
259
+ 2. Go to **API permissions** → verify `email` permission → click **Grant admin consent**
260
+
261
+ ---
262
+
263
+ ## Key Differences from JumpCloud
264
+
265
+ | Aspect | Entra ID | JumpCloud |
266
+ |--------|----------|-----------|
267
+ | **Issuer URL** | `https://login.microsoftonline.com/{tenant-id}/v2.0` | `https://oauth.id.jumpcloud.com/` |
268
+ | **Redirect URI** | Supports `http://localhost` for dev | Requires HTTPS |
269
+ | **SCIM provisioning** | Enterprise App → Provisioning (automatic) | SSO App → Identity Management (SCIM API) |
270
+ | **Provisioning cycles** | Every 40 minutes (or on-demand) | Near real-time |
271
+ | **SCIM quirks** | PascalCase ops, string booleans, mixed-case filters | Mostly spec-compliant |
272
+ | **Free tier** | Free with any Azure account | 10 users forever |
273
+
274
+ ---
275
+
276
+ ## Reference
277
+
278
+ - [Entra ID App Registration - OIDC](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app)
279
+ - [Entra ID SCIM Provisioning](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/use-scim-to-provision-users-and-groups)
280
+ - [Entra ID Optional Claims](https://learn.microsoft.com/en-us/entra/identity-platform/optional-claims)
281
+ - [Entra ID SCIM Known Issues](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/application-provisioning-config-problem-scim-compatibility)