@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,88 @@
|
|
|
1
|
+
import type { User } from '@open-mercato/core/modules/auth/data/entities'
|
|
2
|
+
import type { SsoIdentity, SsoUserDeactivation } from '../data/entities'
|
|
3
|
+
import { coerceBoolean } from './scim-utils'
|
|
4
|
+
|
|
5
|
+
const SCIM_USER_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:User'
|
|
6
|
+
|
|
7
|
+
export interface ScimUserResource {
|
|
8
|
+
schemas: string[]
|
|
9
|
+
id: string
|
|
10
|
+
externalId?: string
|
|
11
|
+
userName: string
|
|
12
|
+
displayName?: string
|
|
13
|
+
name?: { givenName?: string; familyName?: string; formatted?: string }
|
|
14
|
+
emails?: Array<{ value: string; primary: boolean; type: string }>
|
|
15
|
+
active: boolean
|
|
16
|
+
meta: {
|
|
17
|
+
resourceType: string
|
|
18
|
+
created: string
|
|
19
|
+
lastModified: string
|
|
20
|
+
location: string
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function toScimUserResource(
|
|
25
|
+
user: User,
|
|
26
|
+
identity: SsoIdentity,
|
|
27
|
+
baseUrl: string,
|
|
28
|
+
deactivation?: SsoUserDeactivation | null,
|
|
29
|
+
): ScimUserResource {
|
|
30
|
+
const isActive = !deactivation || deactivation.reactivatedAt != null
|
|
31
|
+
|
|
32
|
+
const nameParts = (user.name ?? '').split(' ')
|
|
33
|
+
const givenName = nameParts[0] || undefined
|
|
34
|
+
const familyName = nameParts.length > 1 ? nameParts.slice(1).join(' ') : undefined
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
schemas: [SCIM_USER_SCHEMA],
|
|
38
|
+
id: identity.id,
|
|
39
|
+
...(identity.externalId ? { externalId: identity.externalId } : {}),
|
|
40
|
+
userName: identity.idpEmail,
|
|
41
|
+
displayName: user.name ?? undefined,
|
|
42
|
+
name: (givenName || familyName) ? { givenName, familyName, formatted: user.name ?? undefined } : undefined,
|
|
43
|
+
emails: [{ value: identity.idpEmail, primary: true, type: 'work' }],
|
|
44
|
+
active: isActive,
|
|
45
|
+
meta: {
|
|
46
|
+
resourceType: 'User',
|
|
47
|
+
created: identity.createdAt.toISOString(),
|
|
48
|
+
lastModified: identity.updatedAt.toISOString(),
|
|
49
|
+
location: `${baseUrl}/api/sso/scim/v2/Users/${identity.id}`,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ScimUserPayload {
|
|
55
|
+
userName?: string
|
|
56
|
+
externalId?: string
|
|
57
|
+
displayName?: string
|
|
58
|
+
givenName?: string
|
|
59
|
+
familyName?: string
|
|
60
|
+
email?: string
|
|
61
|
+
active?: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function fromScimUserPayload(payload: Record<string, unknown>): ScimUserPayload {
|
|
65
|
+
const result: ScimUserPayload = {}
|
|
66
|
+
|
|
67
|
+
if (typeof payload.userName === 'string') result.userName = payload.userName
|
|
68
|
+
if (typeof payload.externalId === 'string') result.externalId = payload.externalId
|
|
69
|
+
if (typeof payload.displayName === 'string') result.displayName = payload.displayName
|
|
70
|
+
|
|
71
|
+
if (payload.active !== undefined) {
|
|
72
|
+
result.active = coerceBoolean(payload.active)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const name = payload.name as Record<string, unknown> | undefined
|
|
76
|
+
if (name && typeof name === 'object') {
|
|
77
|
+
if (typeof name.givenName === 'string') result.givenName = name.givenName
|
|
78
|
+
if (typeof name.familyName === 'string') result.familyName = name.familyName
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const emails = payload.emails as Array<Record<string, unknown>> | undefined
|
|
82
|
+
if (Array.isArray(emails) && emails.length > 0) {
|
|
83
|
+
const primary = emails.find((e) => e.primary === true) ?? emails[0]
|
|
84
|
+
if (typeof primary?.value === 'string') result.email = primary.value
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result
|
|
88
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCIM PatchOp parser with Entra ID quirks:
|
|
3
|
+
* - Case-insensitive `op` (e.g., "Replace" vs "replace")
|
|
4
|
+
* - Boolean leniency ("False"/"True" strings → boolean)
|
|
5
|
+
* - Strict attribute allowlist
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { coerceBoolean } from './scim-utils'
|
|
9
|
+
|
|
10
|
+
export interface ScimPatchOperation {
|
|
11
|
+
op: string
|
|
12
|
+
path?: string
|
|
13
|
+
value?: unknown
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ALLOWED_PATHS = new Set([
|
|
17
|
+
'active',
|
|
18
|
+
'displayname',
|
|
19
|
+
'name.givenname',
|
|
20
|
+
'name.familyname',
|
|
21
|
+
'username',
|
|
22
|
+
'externalid',
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
export function parseScimPatchOperations(body: Record<string, unknown>): ScimPatchOperation[] {
|
|
26
|
+
const operations = body.Operations as Array<Record<string, unknown>> | undefined
|
|
27
|
+
if (!Array.isArray(operations)) {
|
|
28
|
+
throw new ScimPatchError('PatchOp body must contain Operations array')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return operations.map((rawOp) => {
|
|
32
|
+
const op = String(rawOp.op ?? '').toLowerCase()
|
|
33
|
+
if (!['add', 'replace', 'remove'].includes(op)) {
|
|
34
|
+
throw new ScimPatchError(`Unsupported SCIM PatchOp: ${rawOp.op}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const path = rawOp.path ? String(rawOp.path) : undefined
|
|
38
|
+
|
|
39
|
+
// Validate path against allowlist if present
|
|
40
|
+
if (path) {
|
|
41
|
+
const normalizedPath = path.toLowerCase()
|
|
42
|
+
if (!ALLOWED_PATHS.has(normalizedPath)) {
|
|
43
|
+
// Silently ignore unsupported attributes (Entra sends many)
|
|
44
|
+
return { op, path, value: undefined }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const value = normalizePatchValue(rawOp.value, path)
|
|
49
|
+
|
|
50
|
+
return { op, path, value }
|
|
51
|
+
}).filter((op) => {
|
|
52
|
+
// Filter out no-ops (unsupported paths where value was set to undefined)
|
|
53
|
+
if (op.op !== 'remove' && op.value === undefined && op.path) return false
|
|
54
|
+
return true
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizePatchValue(value: unknown, path?: string): unknown {
|
|
59
|
+
if (value === undefined || value === null) return value
|
|
60
|
+
|
|
61
|
+
// Handle boolean leniency for the `active` attribute
|
|
62
|
+
if (path && path.toLowerCase() === 'active') {
|
|
63
|
+
return coerceBoolean(value)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Handle value objects (no-path operations)
|
|
67
|
+
if (!path && typeof value === 'object' && value !== null) {
|
|
68
|
+
const obj = value as Record<string, unknown>
|
|
69
|
+
const normalized: Record<string, unknown> = {}
|
|
70
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
71
|
+
if (key.toLowerCase() === 'active') {
|
|
72
|
+
normalized[key] = coerceBoolean(val)
|
|
73
|
+
} else {
|
|
74
|
+
normalized[key] = val
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return normalized
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return value
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class ScimPatchError extends Error {
|
|
84
|
+
constructor(message: string) {
|
|
85
|
+
super(message)
|
|
86
|
+
this.name = 'ScimPatchError'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const SCIM_CONTENT_TYPE = 'application/scim+json'
|
|
2
|
+
|
|
3
|
+
const SCIM_ERROR_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:Error'
|
|
4
|
+
const SCIM_LIST_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:ListResponse'
|
|
5
|
+
|
|
6
|
+
export function scimJson(data: unknown, status = 200): Response {
|
|
7
|
+
return new Response(JSON.stringify(data), {
|
|
8
|
+
status,
|
|
9
|
+
headers: { 'Content-Type': SCIM_CONTENT_TYPE },
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildScimError(
|
|
14
|
+
status: number,
|
|
15
|
+
detail: string,
|
|
16
|
+
scimType?: string,
|
|
17
|
+
): Record<string, unknown> {
|
|
18
|
+
const body: Record<string, unknown> = {
|
|
19
|
+
schemas: [SCIM_ERROR_SCHEMA],
|
|
20
|
+
status: String(status),
|
|
21
|
+
detail,
|
|
22
|
+
}
|
|
23
|
+
if (scimType) body.scimType = scimType
|
|
24
|
+
return body
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildListResponse(
|
|
28
|
+
resources: unknown[],
|
|
29
|
+
totalResults: number,
|
|
30
|
+
startIndex: number,
|
|
31
|
+
itemsPerPage: number,
|
|
32
|
+
): Record<string, unknown> {
|
|
33
|
+
return {
|
|
34
|
+
schemas: [SCIM_LIST_SCHEMA],
|
|
35
|
+
totalResults,
|
|
36
|
+
startIndex,
|
|
37
|
+
itemsPerPage,
|
|
38
|
+
Resources: resources,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import type { SsoFlowState } from './types'
|
|
3
|
+
|
|
4
|
+
const ALGORITHM = 'aes-256-gcm'
|
|
5
|
+
const IV_LENGTH = 12
|
|
6
|
+
const TAG_LENGTH = 16
|
|
7
|
+
const TTL_MS = 5 * 60 * 1000
|
|
8
|
+
const HKDF_SALT = Buffer.from('open-mercato-sso-state-v1')
|
|
9
|
+
const HKDF_INFO = Buffer.from('sso-state-cookie')
|
|
10
|
+
|
|
11
|
+
function deriveKey(secret: string): Buffer {
|
|
12
|
+
return Buffer.from(crypto.hkdfSync('sha256', secret, HKDF_SALT, HKDF_INFO, 32))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getSecret(): string {
|
|
16
|
+
const secret = process.env.SSO_STATE_SECRET || process.env.JWT_SECRET
|
|
17
|
+
if (!secret) throw new Error('SSO_STATE_SECRET or JWT_SECRET must be set')
|
|
18
|
+
return secret
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function encryptStateCookie(payload: SsoFlowState): string {
|
|
22
|
+
const secret = getSecret()
|
|
23
|
+
const key = deriveKey(secret)
|
|
24
|
+
const iv = crypto.randomBytes(IV_LENGTH)
|
|
25
|
+
const json = JSON.stringify(payload)
|
|
26
|
+
|
|
27
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
|
|
28
|
+
const ciphertext = Buffer.concat([cipher.update(json, 'utf8'), cipher.final()])
|
|
29
|
+
const tag = cipher.getAuthTag()
|
|
30
|
+
|
|
31
|
+
const combined = Buffer.concat([iv, tag, ciphertext])
|
|
32
|
+
return combined.toString('base64url')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function decryptStateCookie(cookie: string): SsoFlowState | null {
|
|
36
|
+
try {
|
|
37
|
+
const secret = getSecret()
|
|
38
|
+
const key = deriveKey(secret)
|
|
39
|
+
const combined = Buffer.from(cookie, 'base64url')
|
|
40
|
+
|
|
41
|
+
if (combined.length < IV_LENGTH + TAG_LENGTH) return null
|
|
42
|
+
|
|
43
|
+
const iv = combined.subarray(0, IV_LENGTH)
|
|
44
|
+
const tag = combined.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH)
|
|
45
|
+
const ciphertext = combined.subarray(IV_LENGTH + TAG_LENGTH)
|
|
46
|
+
|
|
47
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
|
|
48
|
+
decipher.setAuthTag(tag)
|
|
49
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8')
|
|
50
|
+
|
|
51
|
+
const payload = JSON.parse(decrypted) as SsoFlowState
|
|
52
|
+
if (payload.expiresAt < Date.now()) return null
|
|
53
|
+
|
|
54
|
+
return payload
|
|
55
|
+
} catch {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createFlowState(params: {
|
|
61
|
+
configId: string
|
|
62
|
+
returnUrl: string
|
|
63
|
+
}): { state: SsoFlowState; codeVerifier: string } {
|
|
64
|
+
const state = crypto.randomBytes(32).toString('base64url')
|
|
65
|
+
const nonce = crypto.randomBytes(16).toString('base64url')
|
|
66
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url')
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
state: {
|
|
70
|
+
state,
|
|
71
|
+
nonce,
|
|
72
|
+
codeVerifier,
|
|
73
|
+
configId: params.configId,
|
|
74
|
+
returnUrl: params.returnUrl,
|
|
75
|
+
expiresAt: Date.now() + TTL_MS,
|
|
76
|
+
},
|
|
77
|
+
codeVerifier,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { SsoConfig } from '../data/entities'
|
|
2
|
+
|
|
3
|
+
export interface SsoProtocolProvider {
|
|
4
|
+
readonly protocol: 'oidc' | 'saml'
|
|
5
|
+
|
|
6
|
+
buildAuthUrl(
|
|
7
|
+
config: SsoConfig,
|
|
8
|
+
params: {
|
|
9
|
+
state: string
|
|
10
|
+
nonce: string
|
|
11
|
+
redirectUri: string
|
|
12
|
+
codeVerifier?: string
|
|
13
|
+
clientSecret?: string
|
|
14
|
+
},
|
|
15
|
+
): Promise<string>
|
|
16
|
+
|
|
17
|
+
handleCallback(
|
|
18
|
+
config: SsoConfig,
|
|
19
|
+
params: {
|
|
20
|
+
callbackParams: Record<string, string>
|
|
21
|
+
redirectUri: string
|
|
22
|
+
expectedState: string
|
|
23
|
+
expectedNonce: string
|
|
24
|
+
codeVerifier?: string
|
|
25
|
+
clientSecret?: string
|
|
26
|
+
},
|
|
27
|
+
): Promise<SsoIdentityPayload>
|
|
28
|
+
|
|
29
|
+
validateConfig(
|
|
30
|
+
config: SsoConfig,
|
|
31
|
+
params?: { clientSecret?: string },
|
|
32
|
+
): Promise<{ ok: boolean; error?: string }>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SsoIdentityPayload {
|
|
36
|
+
subject: string
|
|
37
|
+
email: string
|
|
38
|
+
emailVerified: boolean
|
|
39
|
+
name?: string
|
|
40
|
+
groups?: string[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SsoFlowState {
|
|
44
|
+
state: string
|
|
45
|
+
nonce: string
|
|
46
|
+
codeVerifier: string
|
|
47
|
+
configId: string
|
|
48
|
+
returnUrl: string
|
|
49
|
+
expiresAt: number
|
|
50
|
+
}
|