@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,174 @@
|
|
|
1
|
+
# Google Workspace Setup Guide for Open Mercato SSO
|
|
2
|
+
|
|
3
|
+
This guide walks through setting up Google Workspace as the identity provider for OIDC login in Open Mercato. Google Workspace supports JIT (Just-In-Time) provisioning only — SCIM push provisioning is not available.
|
|
4
|
+
|
|
5
|
+
**Free tier**: Google Cloud OAuth 2.0 is free for internal Workspace applications. No paid APIs required.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Prerequisites
|
|
10
|
+
|
|
11
|
+
- A Google Workspace account with admin access
|
|
12
|
+
- A custom domain verified in Google Workspace (e.g., `company.com`)
|
|
13
|
+
- Access to the Google Cloud Console (https://console.cloud.google.com)
|
|
14
|
+
|
|
15
|
+
## 2. Create a Google Cloud Project
|
|
16
|
+
|
|
17
|
+
1. Go to https://console.cloud.google.com
|
|
18
|
+
2. Click the project selector in the top bar → **New Project**
|
|
19
|
+
3. Name: `Open Mercato SSO` (or your preference)
|
|
20
|
+
4. Click **Create**
|
|
21
|
+
5. Switch to the new project in the project selector
|
|
22
|
+
|
|
23
|
+
## 3. Configure the OAuth Consent Screen
|
|
24
|
+
|
|
25
|
+
1. In the left sidebar, go to **APIs & Services** → **OAuth consent screen**
|
|
26
|
+
2. Select **Internal** (restricts login to your Workspace organization only)
|
|
27
|
+
3. Click **Create**
|
|
28
|
+
4. Fill in:
|
|
29
|
+
|
|
30
|
+
| Field | Value |
|
|
31
|
+
|-------|-------|
|
|
32
|
+
| **App name** | `Open Mercato` |
|
|
33
|
+
| **User support email** | Your admin email |
|
|
34
|
+
| **Authorized domains** | Your Workspace domain (e.g., `company.com`) |
|
|
35
|
+
| **Developer contact email** | Your admin email |
|
|
36
|
+
|
|
37
|
+
5. Click **Save and Continue**
|
|
38
|
+
6. On the **Scopes** step, click **Add or Remove Scopes** and add:
|
|
39
|
+
- `openid`
|
|
40
|
+
- `email`
|
|
41
|
+
- `profile`
|
|
42
|
+
7. Click **Update** → **Save and Continue**
|
|
43
|
+
8. Review and click **Back to Dashboard**
|
|
44
|
+
|
|
45
|
+
## 4. Create OAuth 2.0 Credentials
|
|
46
|
+
|
|
47
|
+
1. Go to **APIs & Services** → **Credentials**
|
|
48
|
+
2. Click **+ Create Credentials** → **OAuth client ID**
|
|
49
|
+
3. Configure:
|
|
50
|
+
|
|
51
|
+
| Field | Value |
|
|
52
|
+
|-------|-------|
|
|
53
|
+
| **Application type** | `Web application` |
|
|
54
|
+
| **Name** | `Open Mercato SSO` |
|
|
55
|
+
| **Authorized redirect URIs** | `http://localhost:3000/api/sso/callback/oidc` |
|
|
56
|
+
|
|
57
|
+
4. Click **Create**
|
|
58
|
+
5. **Copy the Client ID and Client Secret immediately** — you can also retrieve them later from the credentials list
|
|
59
|
+
|
|
60
|
+
### OIDC Credentials Summary
|
|
61
|
+
|
|
62
|
+
| Credential | Value |
|
|
63
|
+
|------------|-------|
|
|
64
|
+
| **Issuer URL** | `https://accounts.google.com` |
|
|
65
|
+
| **Client ID** | Copy from Credentials page |
|
|
66
|
+
| **Client Secret** | Copy from Credentials page |
|
|
67
|
+
| **Redirect URI** | `http://localhost:3000/api/sso/callback/oidc` |
|
|
68
|
+
|
|
69
|
+
**Note**: Google's OIDC discovery document is at `https://accounts.google.com/.well-known/openid-configuration`.
|
|
70
|
+
|
|
71
|
+
## 5. Create the SSO Config in Open Mercato
|
|
72
|
+
|
|
73
|
+
1. Log into Open Mercato as admin
|
|
74
|
+
2. Go to **Settings** → **Single Sign-On** → **Create New**
|
|
75
|
+
3. Select **OIDC** as the protocol
|
|
76
|
+
4. Enter:
|
|
77
|
+
- **Name**: `Google Workspace`
|
|
78
|
+
- **Issuer URL**: `https://accounts.google.com`
|
|
79
|
+
- **Client ID**: (paste from Google Cloud Console)
|
|
80
|
+
- **Client Secret**: (paste from Google Cloud Console)
|
|
81
|
+
5. Add your Workspace domain as an allowed domain (e.g., `company.com`)
|
|
82
|
+
6. Enable **JIT Provisioning** (recommended — creates accounts on first login)
|
|
83
|
+
7. Enable **Auto-link by email** (recommended — links existing accounts by email match)
|
|
84
|
+
8. Click **Verify Discovery** to test the OIDC configuration
|
|
85
|
+
9. Save and activate the configuration
|
|
86
|
+
|
|
87
|
+
## 6. Verify OIDC Login
|
|
88
|
+
|
|
89
|
+
1. Open a private/incognito browser window
|
|
90
|
+
2. Go to the Open Mercato login page
|
|
91
|
+
3. Enter an email address with your Workspace domain (e.g., `user@company.com`)
|
|
92
|
+
4. The login page should detect SSO and show "Continue with SSO"
|
|
93
|
+
5. Click it — you'll be redirected to Google's login page
|
|
94
|
+
6. Authenticate with your Google Workspace account
|
|
95
|
+
7. You should be redirected back to Open Mercato and logged in
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Google Workspace Specifics
|
|
100
|
+
|
|
101
|
+
### No SCIM Provisioning
|
|
102
|
+
|
|
103
|
+
Google Workspace does not support SCIM push provisioning to third-party applications. Users are provisioned via JIT on their first SSO login. The Provisioning tab in the Open Mercato admin UI will show an informational message for Google Workspace configurations.
|
|
104
|
+
|
|
105
|
+
To manage user access:
|
|
106
|
+
- **Provision**: Users are created automatically on first login via JIT
|
|
107
|
+
- **Deprovision**: Remove the user's Workspace account or change their domain to stop SSO access
|
|
108
|
+
|
|
109
|
+
### No Group Claims by Default
|
|
110
|
+
|
|
111
|
+
Google's standard OIDC tokens do not include group membership claims. If you need role-based access from Google groups, you would need to configure a custom claim via Google's Directory API (advanced setup, not covered here).
|
|
112
|
+
|
|
113
|
+
For most setups: leave **Role Mappings** empty in the SSO config. Users will log in with their default assigned role.
|
|
114
|
+
|
|
115
|
+
### `hd` Claim
|
|
116
|
+
|
|
117
|
+
Google OIDC returns a `hd` (hosted domain) claim for Workspace accounts. This identifies the user's organization domain. Open Mercato validates the user's email domain against the allowed domains configured in the SSO config.
|
|
118
|
+
|
|
119
|
+
### `email_verified`
|
|
120
|
+
|
|
121
|
+
Google Workspace accounts always return `email_verified: true`. Personal Gmail accounts may have unverified emails — configuring the consent screen as **Internal** prevents personal accounts from accessing the application.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Troubleshooting
|
|
126
|
+
|
|
127
|
+
### "Access blocked: Open Mercato has not completed the Google verification process"
|
|
128
|
+
|
|
129
|
+
This appears when the consent screen is set to **External** without Google verification. Solution: set the consent screen to **Internal** (Workspace users only).
|
|
130
|
+
|
|
131
|
+
### OIDC login redirects but fails
|
|
132
|
+
|
|
133
|
+
- Verify the Redirect URI matches exactly: `http://localhost:3000/api/sso/callback/oidc`
|
|
134
|
+
- Verify the Issuer URL is `https://accounts.google.com` (not a tenant-specific URL)
|
|
135
|
+
- Ensure `openid`, `email`, and `profile` scopes are configured on the consent screen
|
|
136
|
+
- Check that the user's email domain matches an allowed domain in the SSO config
|
|
137
|
+
|
|
138
|
+
### "Error 400: redirect_uri_mismatch"
|
|
139
|
+
|
|
140
|
+
The redirect URI in the authorization request doesn't match what's registered in Google Cloud Console. Check:
|
|
141
|
+
- `APP_URL` in `.env` matches what you registered (e.g., `http://localhost:3000`)
|
|
142
|
+
- No trailing slash differences
|
|
143
|
+
- Protocol matches (http vs https)
|
|
144
|
+
- The URI is listed in **Authorized redirect URIs**, not **Authorized JavaScript origins**
|
|
145
|
+
|
|
146
|
+
### User gets "No roles could be resolved"
|
|
147
|
+
|
|
148
|
+
This happens when **Role Mappings** are configured but Google doesn't send group claims. Solution: clear the Role Mappings section in the SSO config (leave it empty) to allow login without IdP-based role assignment.
|
|
149
|
+
|
|
150
|
+
### Personal Gmail accounts can log in
|
|
151
|
+
|
|
152
|
+
If you set the consent screen to **External**, any Google account can authenticate. To restrict to your organization only, set the consent screen to **Internal**.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Key Differences from Entra ID
|
|
157
|
+
|
|
158
|
+
| Aspect | Google Workspace | Entra ID |
|
|
159
|
+
|--------|-----------------|----------|
|
|
160
|
+
| **Issuer URL** | `https://accounts.google.com` (same for all orgs) | `https://login.microsoftonline.com/{tenant-id}/v2.0` |
|
|
161
|
+
| **SCIM provisioning** | Not supported | Enterprise App → Provisioning |
|
|
162
|
+
| **Group claims** | Not in standard OIDC tokens | Via optional claims configuration |
|
|
163
|
+
| **User provisioning** | JIT only (on first login) | JIT or SCIM (automatic sync) |
|
|
164
|
+
| **Org restriction** | OAuth consent screen: Internal | App registration: Single tenant |
|
|
165
|
+
| **Free tier** | Free with any Workspace plan | Free with any Azure account |
|
|
166
|
+
| **Redirect URIs** | Supports `http://localhost` for dev | Supports `http://localhost` for dev |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Reference
|
|
171
|
+
|
|
172
|
+
- [Google Cloud OAuth 2.0 Setup](https://developers.google.com/identity/protocols/oauth2)
|
|
173
|
+
- [Google OpenID Connect](https://developers.google.com/identity/openid-connect/openid-connect)
|
|
174
|
+
- [Google Workspace Admin: OAuth Apps](https://support.google.com/a/answer/7281227)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# SSO Module Overview
|
|
2
|
+
|
|
3
|
+
Open Mercato's SSO module provides enterprise-grade Single Sign-On with OIDC and SCIM 2.0 support. This document covers architecture, configuration, and operational guidance.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Supported Identity Providers
|
|
8
|
+
|
|
9
|
+
| IdP | OIDC Login | SCIM Provisioning | JIT Provisioning | Notes |
|
|
10
|
+
|-----|------------|-------------------|------------------|-------|
|
|
11
|
+
| **Microsoft Entra ID** | Yes | Yes | Yes | Full OIDC + SCIM support. See [Entra ID Setup Guide](./entra-id-setup.md) |
|
|
12
|
+
| **Zitadel** | Yes | Yes | Yes | OIDC + SCIM via Actions/native. See [Zitadel Setup Guide](./zitadel-setup.md) |
|
|
13
|
+
| **Google Workspace** | Yes | No | Yes (recommended) | OIDC only, no SCIM push. See [Google Workspace Setup Guide](./google-workspace-setup.md) |
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
### Authentication Flow (OIDC)
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
User → Login Page → HRD Check → IdP Redirect → IdP Login → Callback → Session
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
1. **Home Realm Discovery (HRD)**: User enters email, the system checks if the email domain matches an active SSO config
|
|
26
|
+
2. **Authorization Request**: OIDC Authorization Code + PKCE flow initiated with encrypted state cookie
|
|
27
|
+
3. **IdP Authentication**: User authenticates at the identity provider
|
|
28
|
+
4. **Callback Processing**: Authorization code exchanged for tokens, ID token validated
|
|
29
|
+
5. **Account Linking**: User matched to existing account (by email or SSO subject) or JIT-provisioned
|
|
30
|
+
6. **Session Creation**: Auth session established, user redirected to the application
|
|
31
|
+
|
|
32
|
+
### User Provisioning
|
|
33
|
+
|
|
34
|
+
Two provisioning methods are supported, **mutually exclusive** per SSO config:
|
|
35
|
+
|
|
36
|
+
**JIT (Just-In-Time) Provisioning**
|
|
37
|
+
- Users are created automatically on first OIDC login
|
|
38
|
+
- Profile data extracted from ID token claims
|
|
39
|
+
- Best for: Google Workspace, small organizations, simple setups
|
|
40
|
+
|
|
41
|
+
**SCIM 2.0 Provisioning**
|
|
42
|
+
- Users are pre-provisioned by the IdP before first login
|
|
43
|
+
- Supports create, update, deactivate, and delete operations
|
|
44
|
+
- Best for: Entra ID, large organizations needing lifecycle management
|
|
45
|
+
|
|
46
|
+
### Mutual Exclusivity
|
|
47
|
+
|
|
48
|
+
JIT and SCIM cannot be enabled simultaneously on the same SSO config:
|
|
49
|
+
- Enabling JIT blocks SCIM token creation
|
|
50
|
+
- Creating SCIM tokens blocks enabling JIT
|
|
51
|
+
- Switching requires disabling one before enabling the other
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
### Admin Setup Steps
|
|
58
|
+
|
|
59
|
+
1. **Create SSO Config**: Settings → Single Sign-On → Create New
|
|
60
|
+
2. **Enter IdP Credentials**: Issuer URL, Client ID, Client Secret
|
|
61
|
+
3. **Add Allowed Domains**: Email domains that should use this SSO config
|
|
62
|
+
4. **Choose Provisioning**: Enable JIT or configure SCIM tokens
|
|
63
|
+
5. **Test Connection**: Verify the IdP discovery endpoint is reachable
|
|
64
|
+
6. **Activate**: Enable the config for production use
|
|
65
|
+
|
|
66
|
+
### API Endpoints
|
|
67
|
+
|
|
68
|
+
| Endpoint | Method | Description |
|
|
69
|
+
|----------|--------|-------------|
|
|
70
|
+
| `/api/sso/config` | POST | Create SSO config |
|
|
71
|
+
| `/api/sso/config` | GET | List SSO configs |
|
|
72
|
+
| `/api/sso/config/:id` | GET | Get config by ID |
|
|
73
|
+
| `/api/sso/config/:id` | PUT | Update config |
|
|
74
|
+
| `/api/sso/config/:id` | DELETE | Delete config (must be inactive) |
|
|
75
|
+
| `/api/sso/config/:id/activate` | POST | Activate/deactivate config |
|
|
76
|
+
| `/api/sso/config/:id/domains` | POST | Add domain |
|
|
77
|
+
| `/api/sso/config/:id/domains` | DELETE | Remove domain |
|
|
78
|
+
| `/api/sso/config/:id/test` | POST | Test IdP connection |
|
|
79
|
+
| `/api/sso/hrd` | POST | Home Realm Discovery lookup |
|
|
80
|
+
| `/api/sso/initiate` | GET | Start SSO login flow |
|
|
81
|
+
| `/api/sso/callback/oidc` | GET | OIDC callback |
|
|
82
|
+
| `/api/sso/scim/tokens` | POST | Create SCIM token |
|
|
83
|
+
| `/api/sso/scim/tokens` | GET | List SCIM tokens |
|
|
84
|
+
| `/api/sso/scim/tokens/:id` | DELETE | Revoke SCIM token |
|
|
85
|
+
| `/api/sso/scim/v2/Users` | POST | SCIM: Create user |
|
|
86
|
+
| `/api/sso/scim/v2/Users` | GET | SCIM: List users |
|
|
87
|
+
| `/api/sso/scim/v2/Users/:id` | GET | SCIM: Get user |
|
|
88
|
+
| `/api/sso/scim/v2/Users/:id` | PATCH | SCIM: Update user |
|
|
89
|
+
| `/api/sso/scim/v2/Users/:id` | DELETE | SCIM: Delete user |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Security
|
|
94
|
+
|
|
95
|
+
### OIDC Security Controls
|
|
96
|
+
|
|
97
|
+
| Control | Implementation |
|
|
98
|
+
|---------|---------------|
|
|
99
|
+
| **PKCE** | S256 with 32-byte random code verifier |
|
|
100
|
+
| **State Parameter** | AES-256-GCM encrypted state cookie with HKDF key derivation |
|
|
101
|
+
| **Nonce** | 16-byte random nonce validated in ID token |
|
|
102
|
+
| **State Comparison** | Timing-safe (`crypto.timingSafeEqual`) |
|
|
103
|
+
| **TTL** | 5-minute state cookie lifetime |
|
|
104
|
+
| **CSRF** | SameSite=Lax cookies + encrypted state parameter |
|
|
105
|
+
| **Return URL** | Sanitized to prevent open redirects |
|
|
106
|
+
|
|
107
|
+
### SCIM Security Controls
|
|
108
|
+
|
|
109
|
+
| Control | Implementation |
|
|
110
|
+
|---------|---------------|
|
|
111
|
+
| **Token Format** | `omscim_` prefix + 32 random bytes (hex) |
|
|
112
|
+
| **Storage** | bcrypt-hashed (cost 10), only prefix stored |
|
|
113
|
+
| **One-Time Display** | Raw token returned only at creation |
|
|
114
|
+
| **Timing Attack** | Dummy bcrypt hash on zero candidates |
|
|
115
|
+
| **Tenant Isolation** | Organization ID derived from token, not request |
|
|
116
|
+
|
|
117
|
+
### Data Protection
|
|
118
|
+
|
|
119
|
+
- OIDC client secrets encrypted at rest (AES via `TenantDataEncryptionService`)
|
|
120
|
+
- SCIM tokens bcrypt-hashed, never retrievable after creation
|
|
121
|
+
- No PII in server logs
|
|
122
|
+
- All admin endpoints require authentication + feature-based RBAC
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Provisioning Methods
|
|
127
|
+
|
|
128
|
+
### JIT Provisioning
|
|
129
|
+
|
|
130
|
+
When JIT is enabled on an SSO config:
|
|
131
|
+
|
|
132
|
+
1. User authenticates via OIDC at the IdP
|
|
133
|
+
2. If the user does not exist in Open Mercato, a new account is created
|
|
134
|
+
3. Profile data (name, email) extracted from ID token claims
|
|
135
|
+
4. User is assigned to the organization associated with the SSO config
|
|
136
|
+
5. On subsequent logins, profile data is updated from the latest ID token
|
|
137
|
+
|
|
138
|
+
**Limitations**:
|
|
139
|
+
- User lifecycle not managed (no automatic deactivation)
|
|
140
|
+
- No pre-provisioning (user must log in first)
|
|
141
|
+
- Role assignment requires manual configuration or IdP group claims
|
|
142
|
+
|
|
143
|
+
### SCIM 2.0 Provisioning
|
|
144
|
+
|
|
145
|
+
When SCIM is configured:
|
|
146
|
+
|
|
147
|
+
1. IdP pushes user create/update/delete operations to the SCIM endpoint
|
|
148
|
+
2. Users are pre-provisioned before their first login
|
|
149
|
+
3. Profile changes in the IdP are automatically synced
|
|
150
|
+
4. User deactivation in the IdP triggers deactivation + session revocation
|
|
151
|
+
5. On OIDC login, the existing SCIM-provisioned account is linked (no duplicate)
|
|
152
|
+
|
|
153
|
+
**Supported SCIM Operations**:
|
|
154
|
+
- `POST /Users` — Create user
|
|
155
|
+
- `GET /Users` — List users (with `eq` filter support)
|
|
156
|
+
- `GET /Users/:id` — Get user
|
|
157
|
+
- `PATCH /Users/:id` — Update user (replace operations on `displayName`, `active`, `name.*`, `emails`)
|
|
158
|
+
- `DELETE /Users/:id` — Delete user (soft-delete + deactivation)
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Role Mapping
|
|
163
|
+
|
|
164
|
+
SSO configs support IdP group-to-application role mapping:
|
|
165
|
+
|
|
166
|
+
1. Configure `appRoleMappings` on the SSO config (map IdP group names to app role names)
|
|
167
|
+
2. When the IdP sends group claims in the ID token, roles are automatically assigned
|
|
168
|
+
3. If no mappings are configured, role sync is skipped (user retains existing roles)
|
|
169
|
+
|
|
170
|
+
**Google Workspace note**: Google does not send group claims by default. Role mapping is not available for Google OIDC without additional configuration.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Environment Variables
|
|
175
|
+
|
|
176
|
+
| Variable | Required | Description |
|
|
177
|
+
|----------|----------|-------------|
|
|
178
|
+
| `SSO_STATE_SECRET` | Yes (production) | 32+ byte secret for state cookie encryption |
|
|
179
|
+
| `APP_URL` / `NEXT_PUBLIC_APP_URL` | Recommended | Base URL for redirect URI construction |
|
|
180
|
+
| `SSO_DEV_SEED` | No | Set to `true` to seed demo SSO config in development |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Troubleshooting
|
|
185
|
+
|
|
186
|
+
### Common Issues
|
|
187
|
+
|
|
188
|
+
**"State mismatch — possible CSRF attack"**
|
|
189
|
+
- State cookie expired (5-minute TTL). User took too long at the IdP.
|
|
190
|
+
- Browser blocking third-party cookies. Ensure SameSite=Lax cookies are allowed.
|
|
191
|
+
|
|
192
|
+
**"No roles could be resolved from IdP groups"**
|
|
193
|
+
- Role mappings are configured but the IdP isn't sending matching group claims.
|
|
194
|
+
- Remove role mappings if not needed, or configure the IdP to send group claims.
|
|
195
|
+
|
|
196
|
+
**User created with wrong provisioning method**
|
|
197
|
+
- Check if both JIT and SCIM have been toggled. The system enforces mutual exclusivity.
|
|
198
|
+
- Verify the `provisioningMethod` field on the user's SSO link record.
|
|
199
|
+
|
|
200
|
+
**SCIM requests return 401**
|
|
201
|
+
- Token may be revoked. Check token status in the admin UI.
|
|
202
|
+
- Token format: must include `Authorization: Bearer omscim_...` header.
|
|
203
|
+
- Check that the SSO config is active.
|
|
204
|
+
|
|
205
|
+
**SCIM requests return 403**
|
|
206
|
+
- The SSO config associated with the token is inactive. Activate it first.
|
|
207
|
+
|
|
208
|
+
**HRD not detecting SSO for an email domain**
|
|
209
|
+
- Verify the domain is added to the SSO config's allowed domains.
|
|
210
|
+
- Verify the SSO config is activated (inactive configs are not returned by HRD).
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## IdP-Specific Setup Guides
|
|
215
|
+
|
|
216
|
+
- [Microsoft Entra ID Setup Guide](./entra-id-setup.md) — Full OIDC + SCIM setup
|
|
217
|
+
- [Zitadel Setup Guide](./zitadel-setup.md) — OIDC + SCIM setup
|
|
218
|
+
- [Google Workspace Setup Guide](./google-workspace-setup.md) — OIDC-only setup (JIT recommended)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# SSO Module Security Audit Report
|
|
2
|
+
|
|
3
|
+
**Module:** `packages/enterprise/src/modules/sso/`
|
|
4
|
+
**Date:** 2026-02-27
|
|
5
|
+
**Branch:** `feat/sso-support`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Executive Summary
|
|
10
|
+
|
|
11
|
+
The SSO module demonstrates strong security fundamentals: AES-256-GCM encrypted state cookies, PKCE enforcement, timing-safe state comparison, bcrypt-hashed SCIM tokens, and encrypted OIDC client secrets. The audit uncovered **2 High**, **3 Medium**, and **4 Low** severity findings. Both HIGH findings have been remediated in this milestone.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Findings Summary
|
|
16
|
+
|
|
17
|
+
| # | Finding | Severity | Status |
|
|
18
|
+
|---|---------|----------|--------|
|
|
19
|
+
| F1 | SCIM debug `console.log` statements dump PII to logs | **HIGH** | **FIXED** |
|
|
20
|
+
| F2 | Missing org ownership check in SCIM token generation | **HIGH** | **FIXED** |
|
|
21
|
+
| F3 | SCIM payloads lack string length constraints | MEDIUM | Open |
|
|
22
|
+
| F4 | SCIM logs `ssoConfigId` query param not UUID-validated | MEDIUM | Open |
|
|
23
|
+
| F5 | `emailVerified` defaults to `true` when claim absent | MEDIUM | Documented |
|
|
24
|
+
| F6 | Domain DELETE query param not format-validated | LOW | Open |
|
|
25
|
+
| F7 | SCIM v2 `startIndex`/`count` not zod-validated | LOW | Open |
|
|
26
|
+
| F8 | State cookie not enforced as single-use | LOW | Accepted |
|
|
27
|
+
| F9 | Host header fallback in `toAbsoluteUrl` if `APP_URL` unset | LOW | Accepted |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 1. CSRF Protection -- PASS
|
|
32
|
+
|
|
33
|
+
All admin write endpoints are protected by:
|
|
34
|
+
- Cookie-based auth via `resolveSsoAdminContext()` with `requireAuth: true` and `requireFeatures` guards
|
|
35
|
+
- `SameSite: lax` on auth cookies (appropriate for SSO flows that require cross-site IdP redirects)
|
|
36
|
+
|
|
37
|
+
SCIM v2 endpoints use Bearer token authentication (inherently CSRF-safe).
|
|
38
|
+
|
|
39
|
+
**Files verified:** All routes under `api/config/`, `api/scim/tokens/`, and `api/scim/v2/`.
|
|
40
|
+
|
|
41
|
+
## 2. Replay Protection -- PASS
|
|
42
|
+
|
|
43
|
+
- **State cookie:** AES-256-GCM encrypted with HKDF-SHA256 key derivation, 12-byte random IV, 16-byte auth tag
|
|
44
|
+
- **TTL:** 5 minutes (checked at decryption time + cookie maxAge)
|
|
45
|
+
- **PKCE:** S256 with 32-byte random code verifier stored in encrypted state cookie
|
|
46
|
+
- **Nonce:** 16-byte random nonce validated by `openid-client` library against ID token
|
|
47
|
+
- **State comparison:** Timing-safe (`crypto.timingSafeEqual`) with length pre-check
|
|
48
|
+
- **Cleanup:** State cookie cleared after successful callback (maxAge: 0)
|
|
49
|
+
|
|
50
|
+
**F8 (LOW, Accepted):** No server-side single-use enforcement on state cookie. Mitigated by IdP's single-use authorization code policy (OAuth 2.0 spec requirement).
|
|
51
|
+
|
|
52
|
+
## 3. Tenant Isolation -- PASS (with F2 fixed)
|
|
53
|
+
|
|
54
|
+
- **Admin endpoints:** All queries scoped by `organizationId` for non-superadmins via `resolveSsoAdminContext()`
|
|
55
|
+
- **SCIM endpoints:** `organizationId` derived from verified bearer token, not from request parameters
|
|
56
|
+
- **HRD:** Intentional cross-org lookup (by design) -- only exposes `hasSso`, `configId`, `protocol`
|
|
57
|
+
- **F2 (FIXED):** `ScimTokenService.generateToken()` now verifies SSO config ownership before minting tokens
|
|
58
|
+
|
|
59
|
+
## 4. Token Security -- PASS (with F1 fixed)
|
|
60
|
+
|
|
61
|
+
- **OIDC client secrets:** Encrypted at rest via `TenantDataEncryptionService`; never exposed in API responses
|
|
62
|
+
- **SCIM tokens:** bcrypt-hashed (cost 10), prefix-indexed, timing-attack resistant (dummy hash on miss), raw value returned only once at creation
|
|
63
|
+
- **State cookies:** AES-256-GCM encrypted
|
|
64
|
+
- **F1 (FIXED):** Removed 5 `[SCIM DEBUG]` console.log statements that serialized full SCIM payloads (PII) to logs
|
|
65
|
+
|
|
66
|
+
## 5. Input Validation -- PASS (with notes)
|
|
67
|
+
|
|
68
|
+
- **Admin APIs:** All write endpoints validated with zod schemas from `data/validators.ts`
|
|
69
|
+
- **SCIM filter:** Strict regex parser with attribute allowlist, parameterized ORM queries
|
|
70
|
+
- **Domain validation:** DNS hostname regex, max 253 chars, must contain dot, max 20 per config
|
|
71
|
+
- **Return URL:** `sanitizeReturnUrl()` prevents open redirects (requires `/` prefix, rejects `//`, validates origin)
|
|
72
|
+
|
|
73
|
+
### Open Validation Items
|
|
74
|
+
|
|
75
|
+
- **F3 (MEDIUM):** SCIM payloads parsed via manual extraction without max-length constraints. Database column constraints provide backstop.
|
|
76
|
+
- **F4 (MEDIUM):** SCIM logs `ssoConfigId` param not UUID-validated (DB rejects non-UUID values).
|
|
77
|
+
- **F5 (MEDIUM, Documented):** `emailVerified` defaults to `true` when IdP omits the claim. Acceptable for enterprise IdPs (Entra, Google, Zitadel) which reliably send this claim. Domain allowlist provides mitigating control.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Remediation Status
|
|
82
|
+
|
|
83
|
+
### Completed (This Milestone)
|
|
84
|
+
|
|
85
|
+
1. **F1** -- Removed all `[SCIM DEBUG]` console.log from `api/scim/v2/Users/route.ts` and `api/scim/v2/Users/[id]/route.ts`
|
|
86
|
+
2. **F2** -- Added `organizationId` filter to `ScimTokenService.generateToken()` in `services/scimTokenService.ts`
|
|
87
|
+
3. Removed 3 console.log statements from `lib/oidc-provider.ts` (raw ID token logging)
|
|
88
|
+
|
|
89
|
+
### Future Hardening (Priority 2)
|
|
90
|
+
|
|
91
|
+
4. **F3** -- Add `.slice(0, 255)` length constraints to SCIM mapper fields
|
|
92
|
+
5. **F4** -- Add `z.string().uuid()` validation to SCIM logs endpoint
|
|
93
|
+
6. **F5** -- Document `emailVerified` default behavior in admin guide
|
|
94
|
+
|
|
95
|
+
### Accepted Risks (Priority 3)
|
|
96
|
+
|
|
97
|
+
7. **F6-F9** -- Input validation consistency improvements and defense-in-depth
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Security Controls Summary
|
|
102
|
+
|
|
103
|
+
| Control | Status |
|
|
104
|
+
|---------|--------|
|
|
105
|
+
| Admin inputs validated (zod) | PASS |
|
|
106
|
+
| No hardcoded secrets | PASS |
|
|
107
|
+
| Auth on all admin endpoints | PASS |
|
|
108
|
+
| SCIM Bearer token auth | PASS |
|
|
109
|
+
| Parameterized ORM queries | PASS |
|
|
110
|
+
| HTTPS enforced (production) | PASS |
|
|
111
|
+
| CSRF protection (SameSite + encrypted state) | PASS |
|
|
112
|
+
| Error messages don't leak info | PASS |
|
|
113
|
+
| Client secrets encrypted at rest | PASS |
|
|
114
|
+
| SCIM tokens bcrypt-hashed | PASS |
|
|
115
|
+
| PKCE enforced | PASS |
|
|
116
|
+
| Open redirect protection | PASS |
|
|
117
|
+
| No PII in server logs | PASS (after F1 fix) |
|
|
118
|
+
| Org ownership on token generation | PASS (after F2 fix) |
|