@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.
- package/dist/index.js +1 -1
- package/dist/index.js.map +2 -2
- package/dist/modules/sso/acl.js +11 -0
- package/dist/modules/sso/acl.js.map +7 -0
- package/dist/modules/sso/api/admin-context.js +27 -0
- package/dist/modules/sso/api/admin-context.js.map +7 -0
- package/dist/modules/sso/api/callback/oidc/route.js +103 -0
- package/dist/modules/sso/api/callback/oidc/route.js.map +7 -0
- package/dist/modules/sso/api/config/[id]/activate/route.js +49 -0
- package/dist/modules/sso/api/config/[id]/activate/route.js.map +7 -0
- package/dist/modules/sso/api/config/[id]/domains/route.js +96 -0
- package/dist/modules/sso/api/config/[id]/domains/route.js.map +7 -0
- package/dist/modules/sso/api/config/[id]/route.js +103 -0
- package/dist/modules/sso/api/config/[id]/route.js.map +7 -0
- package/dist/modules/sso/api/config/[id]/test/route.js +41 -0
- package/dist/modules/sso/api/config/[id]/test/route.js.map +7 -0
- package/dist/modules/sso/api/config/route.js +83 -0
- package/dist/modules/sso/api/config/route.js.map +7 -0
- package/dist/modules/sso/api/error-handler.js +28 -0
- package/dist/modules/sso/api/error-handler.js.map +7 -0
- package/dist/modules/sso/api/hrd/route.js +52 -0
- package/dist/modules/sso/api/hrd/route.js.map +7 -0
- package/dist/modules/sso/api/initiate/route.js +66 -0
- package/dist/modules/sso/api/initiate/route.js.map +7 -0
- package/dist/modules/sso/api/scim/context.js +68 -0
- package/dist/modules/sso/api/scim/context.js.map +7 -0
- package/dist/modules/sso/api/scim/logs/route.js +65 -0
- package/dist/modules/sso/api/scim/logs/route.js.map +7 -0
- package/dist/modules/sso/api/scim/tokens/[id]/route.js +42 -0
- package/dist/modules/sso/api/scim/tokens/[id]/route.js.map +7 -0
- package/dist/modules/sso/api/scim/tokens/route.js +83 -0
- package/dist/modules/sso/api/scim/tokens/route.js.map +7 -0
- package/dist/modules/sso/api/scim/v2/ServiceProviderConfig/route.js +42 -0
- package/dist/modules/sso/api/scim/v2/ServiceProviderConfig/route.js.map +7 -0
- package/dist/modules/sso/api/scim/v2/Users/[id]/route.js +94 -0
- package/dist/modules/sso/api/scim/v2/Users/[id]/route.js.map +7 -0
- package/dist/modules/sso/api/scim/v2/Users/route.js +86 -0
- package/dist/modules/sso/api/scim/v2/Users/route.js.map +7 -0
- package/dist/modules/sso/backend/page.js +173 -0
- package/dist/modules/sso/backend/page.js.map +7 -0
- package/dist/modules/sso/backend/page.meta.js +31 -0
- package/dist/modules/sso/backend/page.meta.js.map +7 -0
- package/dist/modules/sso/backend/sso/config/[id]/page.js +749 -0
- package/dist/modules/sso/backend/sso/config/[id]/page.js.map +7 -0
- package/dist/modules/sso/backend/sso/config/[id]/page.meta.js +19 -0
- package/dist/modules/sso/backend/sso/config/[id]/page.meta.js.map +7 -0
- package/dist/modules/sso/backend/sso/config/new/page.js +381 -0
- package/dist/modules/sso/backend/sso/config/new/page.js.map +7 -0
- package/dist/modules/sso/backend/sso/config/new/page.meta.js +19 -0
- package/dist/modules/sso/backend/sso/config/new/page.meta.js.map +7 -0
- package/dist/modules/sso/data/entities.js +299 -0
- package/dist/modules/sso/data/entities.js.map +7 -0
- package/dist/modules/sso/data/validators.js +114 -0
- package/dist/modules/sso/data/validators.js.map +7 -0
- package/dist/modules/sso/di.js +26 -0
- package/dist/modules/sso/di.js.map +7 -0
- package/dist/modules/sso/events.js +24 -0
- package/dist/modules/sso/events.js.map +7 -0
- package/dist/modules/sso/i18n/de.json +146 -0
- package/dist/modules/sso/i18n/en.json +146 -0
- package/dist/modules/sso/i18n/es.json +146 -0
- package/dist/modules/sso/i18n/pl.json +146 -0
- package/dist/modules/sso/index.js +11 -0
- package/dist/modules/sso/index.js.map +7 -0
- package/dist/modules/sso/lib/domains.js +30 -0
- package/dist/modules/sso/lib/domains.js.map +7 -0
- package/dist/modules/sso/lib/oidc-provider.js +140 -0
- package/dist/modules/sso/lib/oidc-provider.js.map +7 -0
- package/dist/modules/sso/lib/registry.js +15 -0
- package/dist/modules/sso/lib/registry.js.map +7 -0
- package/dist/modules/sso/lib/scim-filter.js +43 -0
- package/dist/modules/sso/lib/scim-filter.js.map +7 -0
- package/dist/modules/sso/lib/scim-mapper.js +49 -0
- package/dist/modules/sso/lib/scim-mapper.js.map +7 -0
- package/dist/modules/sso/lib/scim-patch.js +63 -0
- package/dist/modules/sso/lib/scim-patch.js.map +7 -0
- package/dist/modules/sso/lib/scim-response.js +34 -0
- package/dist/modules/sso/lib/scim-response.js.map +7 -0
- package/dist/modules/sso/lib/scim-utils.js +9 -0
- package/dist/modules/sso/lib/scim-utils.js.map +7 -0
- package/dist/modules/sso/lib/state-cookie.js +67 -0
- package/dist/modules/sso/lib/state-cookie.js.map +7 -0
- package/dist/modules/sso/lib/types.js +1 -0
- package/dist/modules/sso/lib/types.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260219000000_sso.js +20 -0
- package/dist/modules/sso/migrations/Migration20260219000000_sso.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260222000000_sso_add_name.js +13 -0
- package/dist/modules/sso/migrations/Migration20260222000000_sso_add_name.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.js +15 -0
- package/dist/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260223000000_scim_tables.js +22 -0
- package/dist/modules/sso/migrations/Migration20260223000000_scim_tables.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260224000000_sso_external_id.js +15 -0
- package/dist/modules/sso/migrations/Migration20260224000000_sso_external_id.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260224100000_sso_role_grants.js +17 -0
- package/dist/modules/sso/migrations/Migration20260224100000_sso_role_grants.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260224200000_drop_default_role_id.js +13 -0
- package/dist/modules/sso/migrations/Migration20260224200000_drop_default_role_id.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.js +23 -0
- package/dist/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.js +14 -0
- package/dist/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.js.map +7 -0
- package/dist/modules/sso/services/accountLinkingService.js +298 -0
- package/dist/modules/sso/services/accountLinkingService.js.map +7 -0
- package/dist/modules/sso/services/hrdService.js +18 -0
- package/dist/modules/sso/services/hrdService.js.map +7 -0
- package/dist/modules/sso/services/scimService.js +372 -0
- package/dist/modules/sso/services/scimService.js.map +7 -0
- package/dist/modules/sso/services/scimTokenService.js +94 -0
- package/dist/modules/sso/services/scimTokenService.js.map +7 -0
- package/dist/modules/sso/services/ssoConfigService.js +254 -0
- package/dist/modules/sso/services/ssoConfigService.js.map +7 -0
- package/dist/modules/sso/services/ssoService.js +125 -0
- package/dist/modules/sso/services/ssoService.js.map +7 -0
- package/dist/modules/sso/setup.js +47 -0
- package/dist/modules/sso/setup.js.map +7 -0
- package/dist/modules/sso/subscribers/user-deleted-cleanup.js +21 -0
- package/dist/modules/sso/subscribers/user-deleted-cleanup.js.map +7 -0
- package/dist/modules/sso/widgets/injection/login-sso/widget.client.js +106 -0
- package/dist/modules/sso/widgets/injection/login-sso/widget.client.js.map +7 -0
- package/dist/modules/sso/widgets/injection/login-sso/widget.js +16 -0
- package/dist/modules/sso/widgets/injection/login-sso/widget.js.map +7 -0
- package/dist/modules/sso/widgets/injection-table.js +14 -0
- package/dist/modules/sso/widgets/injection-table.js.map +7 -0
- package/package.json +5 -4
- package/src/index.ts +1 -1
- package/src/modules/sso/acl.ts +7 -0
- package/src/modules/sso/api/admin-context.ts +36 -0
- package/src/modules/sso/api/callback/oidc/route.ts +115 -0
- package/src/modules/sso/api/config/[id]/activate/route.ts +53 -0
- package/src/modules/sso/api/config/[id]/domains/route.ts +107 -0
- package/src/modules/sso/api/config/[id]/route.ts +114 -0
- package/src/modules/sso/api/config/[id]/test/route.ts +44 -0
- package/src/modules/sso/api/config/route.ts +88 -0
- package/src/modules/sso/api/error-handler.ts +36 -0
- package/src/modules/sso/api/hrd/route.ts +55 -0
- package/src/modules/sso/api/initiate/route.ts +70 -0
- package/src/modules/sso/api/scim/context.ts +85 -0
- package/src/modules/sso/api/scim/logs/route.ts +69 -0
- package/src/modules/sso/api/scim/tokens/[id]/route.ts +45 -0
- package/src/modules/sso/api/scim/tokens/route.ts +89 -0
- package/src/modules/sso/api/scim/v2/ServiceProviderConfig/route.ts +40 -0
- package/src/modules/sso/api/scim/v2/Users/[id]/route.ts +103 -0
- package/src/modules/sso/api/scim/v2/Users/route.ts +94 -0
- package/src/modules/sso/backend/page.meta.ts +29 -0
- package/src/modules/sso/backend/page.tsx +232 -0
- package/src/modules/sso/backend/sso/config/[id]/page.meta.ts +15 -0
- package/src/modules/sso/backend/sso/config/[id]/page.tsx +1024 -0
- package/src/modules/sso/backend/sso/config/new/page.meta.ts +15 -0
- package/src/modules/sso/backend/sso/config/new/page.tsx +463 -0
- package/src/modules/sso/data/entities.ts +240 -0
- package/src/modules/sso/data/validators.ts +140 -0
- package/src/modules/sso/di.ts +25 -0
- package/src/modules/sso/docs/entra-id-setup.md +281 -0
- package/src/modules/sso/docs/google-workspace-setup.md +174 -0
- package/src/modules/sso/docs/sso-overview.md +218 -0
- package/src/modules/sso/docs/sso-security-audit-2026-02-27.md +118 -0
- package/src/modules/sso/docs/zitadel-setup.md +195 -0
- package/src/modules/sso/events.ts +21 -0
- package/src/modules/sso/i18n/de.json +146 -0
- package/src/modules/sso/i18n/en.json +146 -0
- package/src/modules/sso/i18n/es.json +146 -0
- package/src/modules/sso/i18n/pl.json +146 -0
- package/src/modules/sso/index.ts +7 -0
- package/src/modules/sso/lib/domains.ts +31 -0
- package/src/modules/sso/lib/oidc-provider.ts +196 -0
- package/src/modules/sso/lib/registry.ts +13 -0
- package/src/modules/sso/lib/scim-filter.ts +62 -0
- package/src/modules/sso/lib/scim-mapper.ts +88 -0
- package/src/modules/sso/lib/scim-patch.ts +88 -0
- package/src/modules/sso/lib/scim-response.ts +40 -0
- package/src/modules/sso/lib/scim-utils.ts +5 -0
- package/src/modules/sso/lib/state-cookie.ts +79 -0
- package/src/modules/sso/lib/types.ts +50 -0
- package/src/modules/sso/migrations/.snapshot-open-mercato.json +912 -0
- package/src/modules/sso/migrations/Migration20260219000000_sso.ts +21 -0
- package/src/modules/sso/migrations/Migration20260222000000_sso_add_name.ts +13 -0
- package/src/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.ts +15 -0
- package/src/modules/sso/migrations/Migration20260223000000_scim_tables.ts +24 -0
- package/src/modules/sso/migrations/Migration20260224000000_sso_external_id.ts +15 -0
- package/src/modules/sso/migrations/Migration20260224100000_sso_role_grants.ts +18 -0
- package/src/modules/sso/migrations/Migration20260224200000_drop_default_role_id.ts +13 -0
- package/src/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.ts +25 -0
- package/src/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.ts +14 -0
- package/src/modules/sso/services/accountLinkingService.ts +386 -0
- package/src/modules/sso/services/hrdService.ts +22 -0
- package/src/modules/sso/services/scimService.ts +461 -0
- package/src/modules/sso/services/scimTokenService.ts +136 -0
- package/src/modules/sso/services/ssoConfigService.ts +337 -0
- package/src/modules/sso/services/ssoService.ts +167 -0
- package/src/modules/sso/setup.ts +56 -0
- package/src/modules/sso/subscribers/user-deleted-cleanup.ts +33 -0
- package/src/modules/sso/widgets/injection/login-sso/widget.client.tsx +130 -0
- package/src/modules/sso/widgets/injection/login-sso/widget.ts +16 -0
- 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)
|