From bc3025e6fa04b2ae8401e32607d35a24454e392f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 09:12:43 +0000 Subject: [PATCH 1/6] Initial plan From d5831e72c9cd9361441df937d4cc6c888e9109f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 09:19:53 +0000 Subject: [PATCH 2/6] Refactor authentication architecture: separate Auth config from Identity models Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../references/misc/identity/AuthProtocol.mdx | 13 + .../references/misc/identity/AuthProvider.mdx | 16 ++ content/docs/references/system/Account.mdx | 23 ++ content/docs/references/system/AuthConfig.mdx | 1 + .../system/EnterpriseAuthConfig.mdx | 12 + content/docs/references/system/Session.mdx | 18 ++ content/docs/references/system/User.mdx | 16 ++ .../references/system/VerificationToken.mdx | 13 + .../references/system/identity/LDAPConfig.mdx | 13 +- .../references/system/identity/OIDCConfig.mdx | 9 +- .../references/system/identity/SAMLConfig.mdx | 9 +- docs/AUTHENTICATION_STANDARD.md | 151 ++++++++++- packages/spec/json-schema/Account.json | 87 ++++++ packages/spec/json-schema/AuthConfig.json | 166 ++++++++++++ .../json-schema/EnterpriseAuthConfig.json | 172 ++++++++++++ packages/spec/json-schema/LDAPConfig.json | 27 +- packages/spec/json-schema/OIDCConfig.json | 21 +- packages/spec/json-schema/SAMLConfig.json | 20 +- packages/spec/json-schema/Session.json | 59 ++++ .../json-schema/StandardAuthProvider.json | 166 ++++++++++++ packages/spec/json-schema/User.json | 51 ++++ .../spec/json-schema/VerificationToken.json | 36 +++ packages/spec/src/index.ts | 5 +- packages/spec/src/system/auth-protocol.ts | 203 ++++++++++++++ packages/spec/src/system/auth.test.ts | 211 +++++++++++++++ packages/spec/src/system/auth.zod.ts | 93 +++++++ packages/spec/src/system/identity.zod.ts | 251 ++++++++++++++---- 27 files changed, 1784 insertions(+), 78 deletions(-) create mode 100644 content/docs/references/misc/identity/AuthProtocol.mdx create mode 100644 content/docs/references/misc/identity/AuthProvider.mdx create mode 100644 content/docs/references/system/Account.mdx create mode 100644 content/docs/references/system/EnterpriseAuthConfig.mdx create mode 100644 content/docs/references/system/Session.mdx create mode 100644 content/docs/references/system/User.mdx create mode 100644 content/docs/references/system/VerificationToken.mdx create mode 100644 packages/spec/json-schema/Account.json create mode 100644 packages/spec/json-schema/EnterpriseAuthConfig.json create mode 100644 packages/spec/json-schema/Session.json create mode 100644 packages/spec/json-schema/User.json create mode 100644 packages/spec/json-schema/VerificationToken.json create mode 100644 packages/spec/src/system/auth-protocol.ts diff --git a/content/docs/references/misc/identity/AuthProtocol.mdx b/content/docs/references/misc/identity/AuthProtocol.mdx new file mode 100644 index 000000000..7f022beb8 --- /dev/null +++ b/content/docs/references/misc/identity/AuthProtocol.mdx @@ -0,0 +1,13 @@ +--- +title: AuthProtocol +description: AuthProtocol Schema Reference +--- + +## Allowed Values + +* `oidc` +* `saml` +* `ldap` +* `oauth2` +* `local` +* `mock` \ No newline at end of file diff --git a/content/docs/references/misc/identity/AuthProvider.mdx b/content/docs/references/misc/identity/AuthProvider.mdx new file mode 100644 index 000000000..d54bb985d --- /dev/null +++ b/content/docs/references/misc/identity/AuthProvider.mdx @@ -0,0 +1,16 @@ +--- +title: AuthProvider +description: AuthProvider Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Provider ID | +| **label** | `string` | ✅ | Button Label (e.g. "Login with Okta") | +| **type** | `Enum<'oidc' \| 'saml' \| 'ldap' \| 'oauth2' \| 'local' \| 'mock'>` | ✅ | | +| **config** | `object \| object \| object \| Record` | ✅ | Provider specific configuration | +| **icon** | `string` | optional | Icon URL or helper class | +| **active** | `boolean` | optional | | +| **registrationEnabled** | `boolean` | optional | Allow new users to sign up via this provider | diff --git a/content/docs/references/system/Account.mdx b/content/docs/references/system/Account.mdx new file mode 100644 index 000000000..541739381 --- /dev/null +++ b/content/docs/references/system/Account.mdx @@ -0,0 +1,23 @@ +--- +title: Account +description: Account Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique account identifier | +| **userId** | `string` | ✅ | Associated user ID | +| **type** | `Enum<'oauth' \| 'oidc' \| 'email' \| 'credentials' \| 'saml' \| 'ldap'>` | ✅ | Account type | +| **provider** | `string` | ✅ | Provider name | +| **providerAccountId** | `string` | ✅ | Provider account ID | +| **refreshToken** | `string` | optional | OAuth refresh token | +| **accessToken** | `string` | optional | OAuth access token | +| **expiresAt** | `number` | optional | Token expiry timestamp (Unix) | +| **tokenType** | `string` | optional | OAuth token type | +| **scope** | `string` | optional | OAuth scope | +| **idToken** | `string` | optional | OAuth ID token | +| **sessionState** | `string` | optional | Session state | +| **createdAt** | `string` | ✅ | Account creation timestamp | +| **updatedAt** | `string` | ✅ | Last update timestamp | diff --git a/content/docs/references/system/AuthConfig.mdx b/content/docs/references/system/AuthConfig.mdx index 1f7c710da..05f40eb3b 100644 --- a/content/docs/references/system/AuthConfig.mdx +++ b/content/docs/references/system/AuthConfig.mdx @@ -22,6 +22,7 @@ description: AuthConfig Schema Reference | **csrf** | `object` | optional | | | **accountLinking** | `object` | optional | | | **twoFactor** | `object` | optional | | +| **enterprise** | `object` | optional | | | **userFieldMapping** | `object` | optional | | | **database** | `object` | optional | | | **plugins** | `object[]` | optional | | diff --git a/content/docs/references/system/EnterpriseAuthConfig.mdx b/content/docs/references/system/EnterpriseAuthConfig.mdx new file mode 100644 index 000000000..10df64191 --- /dev/null +++ b/content/docs/references/system/EnterpriseAuthConfig.mdx @@ -0,0 +1,12 @@ +--- +title: EnterpriseAuthConfig +description: EnterpriseAuthConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **oidc** | `object` | optional | OpenID Connect configuration | +| **saml** | `object` | optional | SAML 2.0 configuration | +| **ldap** | `object` | optional | LDAP/Active Directory configuration | diff --git a/content/docs/references/system/Session.mdx b/content/docs/references/system/Session.mdx new file mode 100644 index 000000000..dd7d3226b --- /dev/null +++ b/content/docs/references/system/Session.mdx @@ -0,0 +1,18 @@ +--- +title: Session +description: Session Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique session identifier | +| **sessionToken** | `string` | ✅ | Session token | +| **userId** | `string` | ✅ | Associated user ID | +| **expires** | `string` | ✅ | Session expiry timestamp | +| **createdAt** | `string` | ✅ | Session creation timestamp | +| **updatedAt** | `string` | ✅ | Last update timestamp | +| **ipAddress** | `string` | optional | IP address | +| **userAgent** | `string` | optional | User agent string | +| **fingerprint** | `string` | optional | Device fingerprint | diff --git a/content/docs/references/system/User.mdx b/content/docs/references/system/User.mdx new file mode 100644 index 000000000..baae4accc --- /dev/null +++ b/content/docs/references/system/User.mdx @@ -0,0 +1,16 @@ +--- +title: User +description: User Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Unique user identifier | +| **email** | `string` | ✅ | User email address | +| **emailVerified** | `boolean` | optional | Whether email is verified | +| **name** | `string` | optional | User display name | +| **image** | `string` | optional | Profile image URL | +| **createdAt** | `string` | ✅ | Account creation timestamp | +| **updatedAt** | `string` | ✅ | Last update timestamp | diff --git a/content/docs/references/system/VerificationToken.mdx b/content/docs/references/system/VerificationToken.mdx new file mode 100644 index 000000000..ee5f9d034 --- /dev/null +++ b/content/docs/references/system/VerificationToken.mdx @@ -0,0 +1,13 @@ +--- +title: VerificationToken +description: VerificationToken Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **identifier** | `string` | ✅ | Token identifier (email or phone) | +| **token** | `string` | ✅ | Verification token | +| **expires** | `string` | ✅ | Token expiry timestamp | +| **createdAt** | `string` | ✅ | Token creation timestamp | diff --git a/content/docs/references/system/identity/LDAPConfig.mdx b/content/docs/references/system/identity/LDAPConfig.mdx index 6b651678c..1eb470eed 100644 --- a/content/docs/references/system/identity/LDAPConfig.mdx +++ b/content/docs/references/system/identity/LDAPConfig.mdx @@ -7,9 +7,12 @@ description: LDAPConfig Schema Reference | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | | **url** | `string` | ✅ | LDAP Server URL (ldap:// or ldaps://) | -| **bindDn** | `string` | ✅ | | -| **bindCredentials** | `string` | ✅ | | -| **searchBase** | `string` | ✅ | | -| **searchFilter** | `string` | ✅ | | -| **groupSearchBase** | `string` | optional | | +| **bindDn** | `string` | ✅ | Bind DN for LDAP authentication | +| **bindCredentials** | `string` | ✅ | Bind credentials | +| **searchBase** | `string` | ✅ | Search base DN | +| **searchFilter** | `string` | ✅ | Search filter | +| **groupSearchBase** | `string` | optional | Group search base DN | +| **displayName** | `string` | optional | Display name for the provider button | +| **icon** | `string` | optional | Icon URL or identifier | diff --git a/content/docs/references/system/identity/OIDCConfig.mdx b/content/docs/references/system/identity/OIDCConfig.mdx index b74969fe8..6b78913d1 100644 --- a/content/docs/references/system/identity/OIDCConfig.mdx +++ b/content/docs/references/system/identity/OIDCConfig.mdx @@ -7,8 +7,11 @@ description: OIDCConfig Schema Reference | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | | **issuer** | `string` | ✅ | OIDC Issuer URL (.well-known/openid-configuration) | -| **clientId** | `string` | ✅ | | -| **clientSecret** | `string` | ✅ | | -| **scopes** | `string[]` | optional | | +| **clientId** | `string` | ✅ | OIDC client ID | +| **clientSecret** | `string` | ✅ | OIDC client secret | +| **scopes** | `string[]` | optional | OIDC scopes | | **attributeMapping** | `Record` | optional | Map IdP claims to User fields | +| **displayName** | `string` | optional | Display name for the provider button | +| **icon** | `string` | optional | Icon URL or identifier | diff --git a/content/docs/references/system/identity/SAMLConfig.mdx b/content/docs/references/system/identity/SAMLConfig.mdx index 3f38afcf5..6a25ee832 100644 --- a/content/docs/references/system/identity/SAMLConfig.mdx +++ b/content/docs/references/system/identity/SAMLConfig.mdx @@ -7,8 +7,11 @@ description: SAMLConfig Schema Reference | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | | **entryPoint** | `string` | ✅ | IdP SSO URL | -| **cert** | `string` | ✅ | IdP Public Certificate | +| **cert** | `string` | ✅ | IdP Public Certificate (PEM format) | | **issuer** | `string` | ✅ | Entity ID of the IdP | -| **signatureAlgorithm** | `Enum<'sha256' \| 'sha512'>` | optional | | -| **attributeMapping** | `Record` | optional | | +| **signatureAlgorithm** | `Enum<'sha256' \| 'sha512'>` | optional | Signature algorithm | +| **attributeMapping** | `Record` | optional | Map SAML attributes to User fields | +| **displayName** | `string` | optional | Display name for the provider button | +| **icon** | `string` | optional | Icon URL or identifier | diff --git a/docs/AUTHENTICATION_STANDARD.md b/docs/AUTHENTICATION_STANDARD.md index 0c7582c8b..04be91da8 100644 --- a/docs/AUTHENTICATION_STANDARD.md +++ b/docs/AUTHENTICATION_STANDARD.md @@ -26,6 +26,7 @@ The authentication standard can be implemented using various drivers: - **Passkey**: WebAuthn/FIDO2 biometric authentication - **OTP**: One-time password authentication (SMS, Email) - **Anonymous**: Guest/anonymous session support +- **Enterprise SSO**: SAML 2.0, LDAP/Active Directory, and OIDC for enterprise single sign-on ### Security Features @@ -160,6 +161,82 @@ const multiAuthConfig: AuthConfig = { }; ``` +### Enterprise SSO Example + +```typescript +const enterpriseAuthConfig: AuthConfig = { + name: 'enterprise_sso', + label: 'Enterprise SSO', + strategies: ['oauth'], + baseUrl: 'https://app.example.com', + secret: process.env.AUTH_SECRET!, + + // Standard OAuth for consumer providers + oauth: { + providers: [ + { provider: 'google', clientId: '...', clientSecret: '...' }, + ], + }, + + // Enterprise authentication methods + enterprise: { + // OIDC (Modern standard for enterprise SSO) + oidc: { + enabled: true, + issuer: 'https://auth.company.com', + clientId: process.env.OIDC_CLIENT_ID!, + clientSecret: process.env.OIDC_CLIENT_SECRET!, + scopes: ['openid', 'profile', 'email', 'groups'], + attributeMapping: { + email: 'email', + name: 'name', + groups: 'roles', + }, + displayName: 'Login with Corporate SSO', + }, + + // SAML 2.0 (Legacy enterprise SSO) + saml: { + enabled: true, + entryPoint: 'https://idp.company.com/saml/sso', + cert: process.env.SAML_CERT!, + issuer: 'https://idp.company.com', + signatureAlgorithm: 'sha256', + attributeMapping: { + email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', + name: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name', + }, + displayName: 'Login with SAML', + }, + + // LDAP/Active Directory (On-premise) + ldap: { + enabled: true, + url: 'ldaps://ldap.company.com:636', + bindDn: 'CN=Service Account,OU=Users,DC=company,DC=com', + bindCredentials: process.env.LDAP_PASSWORD!, + searchBase: 'OU=Users,DC=company,DC=com', + searchFilter: '(&(objectClass=user)(sAMAccountName={{username}}))', + groupSearchBase: 'OU=Groups,DC=company,DC=com', + displayName: 'Login with Active Directory', + }, + }, + + session: { + expiresIn: 28800, // 8 hours for enterprise + }, + + rateLimit: { + enabled: true, + maxAttempts: 5, + }, + + csrf: { + enabled: true, + }, +}; +``` + ## Configuration Reference ### Core Configuration @@ -283,6 +360,49 @@ twoFactor: { } ``` +### Enterprise Authentication + +```typescript +enterprise: { + // OpenID Connect (Modern enterprise SSO) + oidc?: { + enabled: boolean; // Enable OIDC + issuer: string; // OIDC Issuer URL + clientId: string; // OIDC client ID + clientSecret: string; // OIDC client secret + scopes?: string[]; // OIDC scopes (default: ['openid', 'profile', 'email']) + attributeMapping?: Record; // Map IdP claims to user fields + displayName?: string; // Button label + icon?: string; // Icon URL + }, + + // SAML 2.0 (Legacy enterprise SSO) + saml?: { + enabled: boolean; // Enable SAML + entryPoint: string; // IdP SSO URL + cert: string; // IdP Public Certificate (PEM) + issuer: string; // Entity ID of the IdP + signatureAlgorithm?: 'sha256' | 'sha512'; // Signature algorithm + attributeMapping?: Record; // Map SAML attributes to user fields + displayName?: string; // Button label + icon?: string; // Icon URL + }, + + // LDAP/Active Directory (On-premise) + ldap?: { + enabled: boolean; // Enable LDAP + url: string; // LDAP Server URL (ldap:// or ldaps://) + bindDn: string; // Bind DN + bindCredentials: string; // Bind credentials + searchBase: string; // Search base DN + searchFilter: string; // Search filter + groupSearchBase?: string; // Group search base DN + displayName?: string; // Button label + icon?: string; // Icon URL + } +} +``` + ### Lifecycle Hooks ```typescript @@ -337,7 +457,13 @@ See [examples/auth-better-examples.ts](../examples/auth-better-examples.ts) for ## Schema Files -- **Zod Schema**: `packages/spec/src/system/auth.zod.ts` +The authentication system is organized into three main files: + +1. **Configuration** (`packages/spec/src/system/auth.zod.ts`): Defines **how to login** - OAuth, Email, SAML, LDAP, better-auth driver settings +2. **Data Models** (`packages/spec/src/system/identity.zod.ts`): Defines **who is logged in** - User, Session, Account schemas +3. **Wire Protocol** (`packages/spec/src/system/auth-protocol.ts`): Defines **how to communicate** - API constants, headers, error codes + +Additional files: - **Tests**: `packages/spec/src/system/auth.test.ts` - **JSON Schema**: `packages/spec/json-schema/AuthConfig.json` - **Documentation**: `content/docs/references/system/AuthConfig.mdx` @@ -347,14 +473,37 @@ See [examples/auth-better-examples.ts](../examples/auth-better-examples.ts) for All schemas are defined using Zod and TypeScript types are inferred automatically: ```typescript +// Authentication configuration types import type { AuthConfig, StandardAuthProvider, AuthStrategy, OAuthProvider, SessionConfig, + EnterpriseAuthConfig, + OIDCConfig, + SAMLConfig, + LDAPConfig, // ... and more } from '@objectstack/spec'; + +// User and session data model types +import type { + User, + Account, + Session, + VerificationToken, +} from '@objectstack/spec'; + +// Wire protocol types +import type { + AuthHeaders, + AuthResponse, + AuthError, + TokenPayload, +} from '@objectstack/spec'; + +import { AUTH_CONSTANTS, AUTH_ERROR_CODES } from '@objectstack/spec'; ``` ## Naming Conventions diff --git a/packages/spec/json-schema/Account.json b/packages/spec/json-schema/Account.json new file mode 100644 index 000000000..08ffc535a --- /dev/null +++ b/packages/spec/json-schema/Account.json @@ -0,0 +1,87 @@ +{ + "$ref": "#/definitions/Account", + "definitions": { + "Account": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique account identifier" + }, + "userId": { + "type": "string", + "description": "Associated user ID" + }, + "type": { + "type": "string", + "enum": [ + "oauth", + "oidc", + "email", + "credentials", + "saml", + "ldap" + ], + "description": "Account type" + }, + "provider": { + "type": "string", + "description": "Provider name" + }, + "providerAccountId": { + "type": "string", + "description": "Provider account ID" + }, + "refreshToken": { + "type": "string", + "description": "OAuth refresh token" + }, + "accessToken": { + "type": "string", + "description": "OAuth access token" + }, + "expiresAt": { + "type": "number", + "description": "Token expiry timestamp (Unix)" + }, + "tokenType": { + "type": "string", + "description": "OAuth token type" + }, + "scope": { + "type": "string", + "description": "OAuth scope" + }, + "idToken": { + "type": "string", + "description": "OAuth ID token" + }, + "sessionState": { + "type": "string", + "description": "Session state" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Account creation timestamp" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Last update timestamp" + } + }, + "required": [ + "id", + "userId", + "type", + "provider", + "providerAccountId", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/AuthConfig.json b/packages/spec/json-schema/AuthConfig.json index cc76540b5..94d7ff243 100644 --- a/packages/spec/json-schema/AuthConfig.json +++ b/packages/spec/json-schema/AuthConfig.json @@ -381,6 +381,172 @@ }, "additionalProperties": false }, + "enterprise": { + "type": "object", + "properties": { + "oidc": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "format": "uri", + "description": "OIDC Issuer URL (.well-known/openid-configuration)" + }, + "clientId": { + "type": "string", + "description": "OIDC client ID" + }, + "clientSecret": { + "type": "string", + "description": "OIDC client secret" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "openid", + "profile", + "email" + ], + "description": "OIDC scopes" + }, + "attributeMapping": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Map IdP claims to User fields" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "issuer", + "clientId", + "clientSecret" + ], + "additionalProperties": false, + "description": "OpenID Connect configuration" + }, + "saml": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "entryPoint": { + "type": "string", + "format": "uri", + "description": "IdP SSO URL" + }, + "cert": { + "type": "string", + "description": "IdP Public Certificate (PEM format)" + }, + "issuer": { + "type": "string", + "description": "Entity ID of the IdP" + }, + "signatureAlgorithm": { + "type": "string", + "enum": [ + "sha256", + "sha512" + ], + "default": "sha256", + "description": "Signature algorithm" + }, + "attributeMapping": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Map SAML attributes to User fields" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "entryPoint", + "cert", + "issuer" + ], + "additionalProperties": false, + "description": "SAML 2.0 configuration" + }, + "ldap": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "url": { + "type": "string", + "format": "uri", + "description": "LDAP Server URL (ldap:// or ldaps://)" + }, + "bindDn": { + "type": "string", + "description": "Bind DN for LDAP authentication" + }, + "bindCredentials": { + "type": "string", + "description": "Bind credentials" + }, + "searchBase": { + "type": "string", + "description": "Search base DN" + }, + "searchFilter": { + "type": "string", + "description": "Search filter" + }, + "groupSearchBase": { + "type": "string", + "description": "Group search base DN" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "url", + "bindDn", + "bindCredentials", + "searchBase", + "searchFilter" + ], + "additionalProperties": false, + "description": "LDAP/Active Directory configuration" + } + }, + "additionalProperties": false + }, "userFieldMapping": { "type": "object", "properties": { diff --git a/packages/spec/json-schema/EnterpriseAuthConfig.json b/packages/spec/json-schema/EnterpriseAuthConfig.json new file mode 100644 index 000000000..e57dd05d5 --- /dev/null +++ b/packages/spec/json-schema/EnterpriseAuthConfig.json @@ -0,0 +1,172 @@ +{ + "$ref": "#/definitions/EnterpriseAuthConfig", + "definitions": { + "EnterpriseAuthConfig": { + "type": "object", + "properties": { + "oidc": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "format": "uri", + "description": "OIDC Issuer URL (.well-known/openid-configuration)" + }, + "clientId": { + "type": "string", + "description": "OIDC client ID" + }, + "clientSecret": { + "type": "string", + "description": "OIDC client secret" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "openid", + "profile", + "email" + ], + "description": "OIDC scopes" + }, + "attributeMapping": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Map IdP claims to User fields" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "issuer", + "clientId", + "clientSecret" + ], + "additionalProperties": false, + "description": "OpenID Connect configuration" + }, + "saml": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "entryPoint": { + "type": "string", + "format": "uri", + "description": "IdP SSO URL" + }, + "cert": { + "type": "string", + "description": "IdP Public Certificate (PEM format)" + }, + "issuer": { + "type": "string", + "description": "Entity ID of the IdP" + }, + "signatureAlgorithm": { + "type": "string", + "enum": [ + "sha256", + "sha512" + ], + "default": "sha256", + "description": "Signature algorithm" + }, + "attributeMapping": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Map SAML attributes to User fields" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "entryPoint", + "cert", + "issuer" + ], + "additionalProperties": false, + "description": "SAML 2.0 configuration" + }, + "ldap": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "url": { + "type": "string", + "format": "uri", + "description": "LDAP Server URL (ldap:// or ldaps://)" + }, + "bindDn": { + "type": "string", + "description": "Bind DN for LDAP authentication" + }, + "bindCredentials": { + "type": "string", + "description": "Bind credentials" + }, + "searchBase": { + "type": "string", + "description": "Search base DN" + }, + "searchFilter": { + "type": "string", + "description": "Search filter" + }, + "groupSearchBase": { + "type": "string", + "description": "Group search base DN" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "url", + "bindDn", + "bindCredentials", + "searchBase", + "searchFilter" + ], + "additionalProperties": false, + "description": "LDAP/Active Directory configuration" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/LDAPConfig.json b/packages/spec/json-schema/LDAPConfig.json index a5f21b108..3c7092cda 100644 --- a/packages/spec/json-schema/LDAPConfig.json +++ b/packages/spec/json-schema/LDAPConfig.json @@ -4,25 +4,42 @@ "LDAPConfig": { "type": "object", "properties": { + "enabled": { + "type": "boolean", + "default": false + }, "url": { "type": "string", "format": "uri", "description": "LDAP Server URL (ldap:// or ldaps://)" }, "bindDn": { - "type": "string" + "type": "string", + "description": "Bind DN for LDAP authentication" }, "bindCredentials": { - "type": "string" + "type": "string", + "description": "Bind credentials" }, "searchBase": { - "type": "string" + "type": "string", + "description": "Search base DN" }, "searchFilter": { - "type": "string" + "type": "string", + "description": "Search filter" }, "groupSearchBase": { - "type": "string" + "type": "string", + "description": "Group search base DN" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" } }, "required": [ diff --git a/packages/spec/json-schema/OIDCConfig.json b/packages/spec/json-schema/OIDCConfig.json index 2afdfd116..cc4a22a7b 100644 --- a/packages/spec/json-schema/OIDCConfig.json +++ b/packages/spec/json-schema/OIDCConfig.json @@ -4,16 +4,22 @@ "OIDCConfig": { "type": "object", "properties": { + "enabled": { + "type": "boolean", + "default": false + }, "issuer": { "type": "string", "format": "uri", "description": "OIDC Issuer URL (.well-known/openid-configuration)" }, "clientId": { - "type": "string" + "type": "string", + "description": "OIDC client ID" }, "clientSecret": { - "type": "string" + "type": "string", + "description": "OIDC client secret" }, "scopes": { "type": "array", @@ -24,7 +30,8 @@ "openid", "profile", "email" - ] + ], + "description": "OIDC scopes" }, "attributeMapping": { "type": "object", @@ -32,6 +39,14 @@ "type": "string" }, "description": "Map IdP claims to User fields" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" } }, "required": [ diff --git a/packages/spec/json-schema/SAMLConfig.json b/packages/spec/json-schema/SAMLConfig.json index f0ce7a90f..ae02b5b83 100644 --- a/packages/spec/json-schema/SAMLConfig.json +++ b/packages/spec/json-schema/SAMLConfig.json @@ -4,6 +4,10 @@ "SAMLConfig": { "type": "object", "properties": { + "enabled": { + "type": "boolean", + "default": false + }, "entryPoint": { "type": "string", "format": "uri", @@ -11,7 +15,7 @@ }, "cert": { "type": "string", - "description": "IdP Public Certificate" + "description": "IdP Public Certificate (PEM format)" }, "issuer": { "type": "string", @@ -23,13 +27,23 @@ "sha256", "sha512" ], - "default": "sha256" + "default": "sha256", + "description": "Signature algorithm" }, "attributeMapping": { "type": "object", "additionalProperties": { "type": "string" - } + }, + "description": "Map SAML attributes to User fields" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" } }, "required": [ diff --git a/packages/spec/json-schema/Session.json b/packages/spec/json-schema/Session.json new file mode 100644 index 000000000..fe11b29f1 --- /dev/null +++ b/packages/spec/json-schema/Session.json @@ -0,0 +1,59 @@ +{ + "$ref": "#/definitions/Session", + "definitions": { + "Session": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique session identifier" + }, + "sessionToken": { + "type": "string", + "description": "Session token" + }, + "userId": { + "type": "string", + "description": "Associated user ID" + }, + "expires": { + "type": "string", + "format": "date-time", + "description": "Session expiry timestamp" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Session creation timestamp" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Last update timestamp" + }, + "ipAddress": { + "type": "string", + "description": "IP address" + }, + "userAgent": { + "type": "string", + "description": "User agent string" + }, + "fingerprint": { + "type": "string", + "description": "Device fingerprint" + } + }, + "required": [ + "id", + "sessionToken", + "userId", + "expires", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/StandardAuthProvider.json b/packages/spec/json-schema/StandardAuthProvider.json index 67753239b..f34b0d5ad 100644 --- a/packages/spec/json-schema/StandardAuthProvider.json +++ b/packages/spec/json-schema/StandardAuthProvider.json @@ -389,6 +389,172 @@ }, "additionalProperties": false }, + "enterprise": { + "type": "object", + "properties": { + "oidc": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "format": "uri", + "description": "OIDC Issuer URL (.well-known/openid-configuration)" + }, + "clientId": { + "type": "string", + "description": "OIDC client ID" + }, + "clientSecret": { + "type": "string", + "description": "OIDC client secret" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "openid", + "profile", + "email" + ], + "description": "OIDC scopes" + }, + "attributeMapping": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Map IdP claims to User fields" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "issuer", + "clientId", + "clientSecret" + ], + "additionalProperties": false, + "description": "OpenID Connect configuration" + }, + "saml": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "entryPoint": { + "type": "string", + "format": "uri", + "description": "IdP SSO URL" + }, + "cert": { + "type": "string", + "description": "IdP Public Certificate (PEM format)" + }, + "issuer": { + "type": "string", + "description": "Entity ID of the IdP" + }, + "signatureAlgorithm": { + "type": "string", + "enum": [ + "sha256", + "sha512" + ], + "default": "sha256", + "description": "Signature algorithm" + }, + "attributeMapping": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Map SAML attributes to User fields" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "entryPoint", + "cert", + "issuer" + ], + "additionalProperties": false, + "description": "SAML 2.0 configuration" + }, + "ldap": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "url": { + "type": "string", + "format": "uri", + "description": "LDAP Server URL (ldap:// or ldaps://)" + }, + "bindDn": { + "type": "string", + "description": "Bind DN for LDAP authentication" + }, + "bindCredentials": { + "type": "string", + "description": "Bind credentials" + }, + "searchBase": { + "type": "string", + "description": "Search base DN" + }, + "searchFilter": { + "type": "string", + "description": "Search filter" + }, + "groupSearchBase": { + "type": "string", + "description": "Group search base DN" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "url", + "bindDn", + "bindCredentials", + "searchBase", + "searchFilter" + ], + "additionalProperties": false, + "description": "LDAP/Active Directory configuration" + } + }, + "additionalProperties": false + }, "userFieldMapping": { "type": "object", "properties": { diff --git a/packages/spec/json-schema/User.json b/packages/spec/json-schema/User.json new file mode 100644 index 000000000..dd3048079 --- /dev/null +++ b/packages/spec/json-schema/User.json @@ -0,0 +1,51 @@ +{ + "$ref": "#/definitions/User", + "definitions": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique user identifier" + }, + "email": { + "type": "string", + "format": "email", + "description": "User email address" + }, + "emailVerified": { + "type": "boolean", + "default": false, + "description": "Whether email is verified" + }, + "name": { + "type": "string", + "description": "User display name" + }, + "image": { + "type": "string", + "format": "uri", + "description": "Profile image URL" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Account creation timestamp" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Last update timestamp" + } + }, + "required": [ + "id", + "email", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/VerificationToken.json b/packages/spec/json-schema/VerificationToken.json new file mode 100644 index 000000000..8e40199c8 --- /dev/null +++ b/packages/spec/json-schema/VerificationToken.json @@ -0,0 +1,36 @@ +{ + "$ref": "#/definitions/VerificationToken", + "definitions": { + "VerificationToken": { + "type": "object", + "properties": { + "identifier": { + "type": "string", + "description": "Token identifier (email or phone)" + }, + "token": { + "type": "string", + "description": "Verification token" + }, + "expires": { + "type": "string", + "format": "date-time", + "description": "Token expiry timestamp" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Token creation timestamp" + } + }, + "required": [ + "identifier", + "token", + "expires", + "createdAt" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/src/index.ts b/packages/spec/src/index.ts index 1bc27b4d5..db5504553 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -41,8 +41,9 @@ export * from './ui/theme.zod'; export * from './system/manifest.zod'; export * from './system/datasource.zod'; export * from './system/api.zod'; -export * from './system/identity.zod'; -export * from './system/auth.zod'; +export * from './system/identity.zod'; // User, Account, Session models +export * from './system/auth.zod'; // Authentication configuration +export * from './system/auth-protocol'; // Authentication wire protocol & constants export * from './system/policy.zod'; export * from './system/role.zod'; export * from './system/territory.zod'; diff --git a/packages/spec/src/system/auth-protocol.ts b/packages/spec/src/system/auth-protocol.ts new file mode 100644 index 000000000..bfa73da36 --- /dev/null +++ b/packages/spec/src/system/auth-protocol.ts @@ -0,0 +1,203 @@ +/** + * Authentication Wire Protocol & Constants + * + * Defines the API contract and constants for authentication communication. + * These constants ensure consistent behavior across all ObjectStack implementations. + */ + +/** + * Authentication Constants + * Standard headers, prefixes, and identifiers for auth communication + */ +export const AUTH_CONSTANTS = { + /** + * HTTP header key for authentication tokens + */ + HEADER_KEY: 'Authorization', + + /** + * Token prefix for Bearer authentication + */ + TOKEN_PREFIX: 'Bearer ', + + /** + * Cookie prefix for ObjectStack auth cookies + */ + COOKIE_PREFIX: 'os_', + + /** + * CSRF token header name + */ + CSRF_HEADER: 'x-os-csrf-token', + + /** + * Default session cookie name + */ + SESSION_COOKIE: 'os_session_token', + + /** + * Default CSRF cookie name + */ + CSRF_COOKIE: 'os_csrf_token', + + /** + * Refresh token cookie name + */ + REFRESH_TOKEN_COOKIE: 'os_refresh_token', +} as const; + +/** + * Authentication Headers Interface + * Standard headers used in authenticated requests + */ +export interface AuthHeaders { + /** + * Authorization header with Bearer token + * @example "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + */ + Authorization?: string; + + /** + * CSRF token header + * @example "x-os-csrf-token: abc123def456..." + */ + 'x-os-csrf-token'?: string; + + /** + * Session ID header (alternative to cookie) + * @example "x-os-session-id: session_abc123..." + */ + 'x-os-session-id'?: string; + + /** + * API key header (for service-to-service auth) + * @example "x-os-api-key: sk_live_abc123..." + */ + 'x-os-api-key'?: string; +} + +/** + * Authentication Response Interface + * Standard response format for authentication operations + */ +export interface AuthResponse { + /** + * Access token (JWT or opaque token) + */ + accessToken: string; + + /** + * Refresh token (for token renewal) + */ + refreshToken?: string; + + /** + * Token type (usually "Bearer") + */ + tokenType: string; + + /** + * Token expiry in seconds + */ + expiresIn: number; + + /** + * User information + */ + user: { + id: string; + email: string; + name?: string; + image?: string; + }; + + /** + * Session ID + */ + sessionId?: string; +} + +/** + * Authentication Error Codes + * Standard error codes for authentication failures + */ +export const AUTH_ERROR_CODES = { + INVALID_CREDENTIALS: 'invalid_credentials', + INVALID_TOKEN: 'invalid_token', + TOKEN_EXPIRED: 'token_expired', + INSUFFICIENT_PERMISSIONS: 'insufficient_permissions', + ACCOUNT_LOCKED: 'account_locked', + ACCOUNT_NOT_VERIFIED: 'account_not_verified', + TOO_MANY_REQUESTS: 'too_many_requests', + INVALID_CSRF_TOKEN: 'invalid_csrf_token', + SESSION_EXPIRED: 'session_expired', + OAUTH_ERROR: 'oauth_error', + PROVIDER_ERROR: 'provider_error', +} as const; + +/** + * Authentication Error Interface + * Standard error response format + */ +export interface AuthError { + /** + * Error code from AUTH_ERROR_CODES + */ + code: typeof AUTH_ERROR_CODES[keyof typeof AUTH_ERROR_CODES]; + + /** + * Human-readable error message + */ + message: string; + + /** + * Additional error details + */ + details?: Record; +} + +/** + * Token Payload Interface + * Standard JWT payload structure + */ +export interface TokenPayload { + /** + * Subject (user ID) + */ + sub: string; + + /** + * Issued at timestamp + */ + iat: number; + + /** + * Expiration timestamp + */ + exp: number; + + /** + * Session ID + */ + sid?: string; + + /** + * User email + */ + email?: string; + + /** + * User roles + */ + roles?: string[]; + + /** + * User permissions + */ + permissions?: string[]; + + /** + * Additional custom claims + */ + [key: string]: any; +} diff --git a/packages/spec/src/system/auth.test.ts b/packages/spec/src/system/auth.test.ts index e63d1c4f7..9968273f5 100644 --- a/packages/spec/src/system/auth.test.ts +++ b/packages/spec/src/system/auth.test.ts @@ -10,6 +10,10 @@ import { CSRFConfigSchema, AccountLinkingConfigSchema, TwoFactorConfigSchema, + OIDCConfigSchema, + SAMLConfigSchema, + LDAPConfigSchema, + EnterpriseAuthConfigSchema, UserFieldMappingSchema, DatabaseAdapterSchema, AuthPluginConfigSchema, @@ -311,6 +315,183 @@ describe('TwoFactorConfigSchema', () => { }); }); +describe('OIDCConfigSchema', () => { + it('should accept valid OIDC configuration', () => { + const config = { + enabled: true, + issuer: 'https://auth.example.com', + clientId: 'client-id', + clientSecret: 'client-secret', + scopes: ['openid', 'profile', 'email', 'groups'], + attributeMapping: { + email: 'email', + name: 'name', + }, + }; + + expect(() => OIDCConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = { + issuer: 'https://auth.example.com', + clientId: 'client-id', + clientSecret: 'client-secret', + }; + const result = OIDCConfigSchema.parse(config); + + expect(result.enabled).toBe(false); + expect(result.scopes).toEqual(['openid', 'profile', 'email']); + }); + + it('should validate issuer is a URL', () => { + const config = { + issuer: 'not-a-url', + clientId: 'client-id', + clientSecret: 'client-secret', + }; + + expect(() => OIDCConfigSchema.parse(config)).toThrow(); + }); +}); + +describe('SAMLConfigSchema', () => { + it('should accept valid SAML configuration', () => { + const config = { + enabled: true, + entryPoint: 'https://idp.example.com/saml/sso', + cert: '-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----', + issuer: 'https://idp.example.com', + signatureAlgorithm: 'sha256' as const, + attributeMapping: { + email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', + }, + }; + + expect(() => SAMLConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = { + entryPoint: 'https://idp.example.com/saml/sso', + cert: 'cert-content', + issuer: 'https://idp.example.com', + }; + const result = SAMLConfigSchema.parse(config); + + expect(result.enabled).toBe(false); + expect(result.signatureAlgorithm).toBe('sha256'); + }); + + it('should accept sha512 signature algorithm', () => { + const config = { + entryPoint: 'https://idp.example.com/saml/sso', + cert: 'cert-content', + issuer: 'https://idp.example.com', + signatureAlgorithm: 'sha512' as const, + }; + + expect(() => SAMLConfigSchema.parse(config)).not.toThrow(); + }); +}); + +describe('LDAPConfigSchema', () => { + it('should accept valid LDAP configuration', () => { + const config = { + enabled: true, + url: 'ldaps://ldap.example.com:636', + bindDn: 'CN=Service Account,OU=Users,DC=example,DC=com', + bindCredentials: 'password', + searchBase: 'OU=Users,DC=example,DC=com', + searchFilter: '(&(objectClass=user)(sAMAccountName={{username}}))', + groupSearchBase: 'OU=Groups,DC=example,DC=com', + }; + + expect(() => LDAPConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = { + url: 'ldap://ldap.example.com:389', + bindDn: 'CN=Service Account,OU=Users,DC=example,DC=com', + bindCredentials: 'password', + searchBase: 'OU=Users,DC=example,DC=com', + searchFilter: '(uid={{username}})', + }; + const result = LDAPConfigSchema.parse(config); + + expect(result.enabled).toBe(false); + }); + + it('should accept ldap:// and ldaps:// URLs', () => { + const ldapConfig = { + url: 'ldap://ldap.example.com:389', + bindDn: 'cn=admin', + bindCredentials: 'password', + searchBase: 'dc=example,dc=com', + searchFilter: '(uid={{username}})', + }; + + const ldapsConfig = { + url: 'ldaps://ldap.example.com:636', + bindDn: 'cn=admin', + bindCredentials: 'password', + searchBase: 'dc=example,dc=com', + searchFilter: '(uid={{username}})', + }; + + expect(() => LDAPConfigSchema.parse(ldapConfig)).not.toThrow(); + expect(() => LDAPConfigSchema.parse(ldapsConfig)).not.toThrow(); + }); +}); + +describe('EnterpriseAuthConfigSchema', () => { + it('should accept enterprise config with all providers', () => { + const config = { + oidc: { + enabled: true, + issuer: 'https://auth.example.com', + clientId: 'client-id', + clientSecret: 'client-secret', + }, + saml: { + enabled: true, + entryPoint: 'https://idp.example.com/saml/sso', + cert: 'cert-content', + issuer: 'https://idp.example.com', + }, + ldap: { + enabled: true, + url: 'ldaps://ldap.example.com:636', + bindDn: 'cn=admin', + bindCredentials: 'password', + searchBase: 'dc=example,dc=com', + searchFilter: '(uid={{username}})', + }, + }; + + expect(() => EnterpriseAuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept partial enterprise config', () => { + const config = { + oidc: { + enabled: true, + issuer: 'https://auth.example.com', + clientId: 'client-id', + clientSecret: 'client-secret', + }, + }; + + expect(() => EnterpriseAuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept empty enterprise config', () => { + const config = {}; + expect(() => EnterpriseAuthConfigSchema.parse(config)).not.toThrow(); + }); +}); + describe('UserFieldMappingSchema', () => { it('should accept custom field mappings', () => { const config = { @@ -644,6 +825,36 @@ describe('AuthConfigSchema', () => { expect(() => AuthConfigSchema.parse(config)).not.toThrow(); }); + it('should accept configuration with enterprise authentication', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + enterprise: { + oidc: { + enabled: true, + issuer: 'https://auth.example.com', + clientId: 'oidc-client-id', + clientSecret: 'oidc-client-secret', + }, + saml: { + enabled: true, + entryPoint: 'https://idp.example.com/saml/sso', + cert: 'saml-cert', + issuer: 'https://idp.example.com', + }, + }, + }; + + expect(() => AuthConfigSchema.parse(config)).not.toThrow(); + }); + it('should use default values for optional fields', () => { const config = { name: 'test_auth', diff --git a/packages/spec/src/system/auth.zod.ts b/packages/spec/src/system/auth.zod.ts index e00b12d90..b86d71e15 100644 --- a/packages/spec/src/system/auth.zod.ts +++ b/packages/spec/src/system/auth.zod.ts @@ -205,6 +205,94 @@ export const TwoFactorConfigSchema = z.object({ export type TwoFactorConfig = z.infer; +/** + * OIDC / OAuth2 Enterprise Configuration + * OpenID Connect configuration for enterprise SSO + */ +export const OIDCConfigSchema = z.object({ + enabled: z.boolean().default(false), + + issuer: z.string().url().describe('OIDC Issuer URL (.well-known/openid-configuration)'), + + clientId: z.string().describe('OIDC client ID'), + + clientSecret: z.string().describe('OIDC client secret'), + + scopes: z.array(z.string()).default(['openid', 'profile', 'email']).describe('OIDC scopes'), + + attributeMapping: z.record(z.string()).optional().describe('Map IdP claims to User fields'), + + displayName: z.string().optional().describe('Display name for the provider button'), + + icon: z.string().optional().describe('Icon URL or identifier'), +}); + +export type OIDCConfig = z.infer; + +/** + * SAML 2.0 Enterprise Configuration + * SAML configuration for legacy enterprise SSO + */ +export const SAMLConfigSchema = z.object({ + enabled: z.boolean().default(false), + + entryPoint: z.string().url().describe('IdP SSO URL'), + + cert: z.string().describe('IdP Public Certificate (PEM format)'), + + issuer: z.string().describe('Entity ID of the IdP'), + + signatureAlgorithm: z.enum(['sha256', 'sha512']).default('sha256').describe('Signature algorithm'), + + attributeMapping: z.record(z.string()).optional().describe('Map SAML attributes to User fields'), + + displayName: z.string().optional().describe('Display name for the provider button'), + + icon: z.string().optional().describe('Icon URL or identifier'), +}); + +export type SAMLConfig = z.infer; + +/** + * LDAP / Active Directory Enterprise Configuration + * LDAP configuration for on-premise directory services + */ +export const LDAPConfigSchema = z.object({ + enabled: z.boolean().default(false), + + url: z.string().url().describe('LDAP Server URL (ldap:// or ldaps://)'), + + bindDn: z.string().describe('Bind DN for LDAP authentication'), + + bindCredentials: z.string().describe('Bind credentials'), + + searchBase: z.string().describe('Search base DN'), + + searchFilter: z.string().describe('Search filter'), + + groupSearchBase: z.string().optional().describe('Group search base DN'), + + displayName: z.string().optional().describe('Display name for the provider button'), + + icon: z.string().optional().describe('Icon URL or identifier'), +}); + +export type LDAPConfig = z.infer; + +/** + * Enterprise Authentication Configuration + * Combines SAML, LDAP, and OIDC configurations for enterprise SSO + */ +export const EnterpriseAuthConfigSchema = z.object({ + oidc: OIDCConfigSchema.optional().describe('OpenID Connect configuration'), + + saml: SAMLConfigSchema.optional().describe('SAML 2.0 configuration'), + + ldap: LDAPConfigSchema.optional().describe('LDAP/Active Directory configuration'), +}); + +export type EnterpriseAuthConfig = z.infer; + /** * User Field Mapping Configuration * Maps authentication user fields to ObjectStack user object fields @@ -366,6 +454,11 @@ export const AuthConfigSchema = z.object({ */ twoFactor: TwoFactorConfigSchema.optional(), + /** + * Enterprise authentication configuration (SAML, LDAP, OIDC) + */ + enterprise: EnterpriseAuthConfigSchema.optional(), + /** * User field mapping */ diff --git a/packages/spec/src/system/identity.zod.ts b/packages/spec/src/system/identity.zod.ts index 440ebbb5e..655b25222 100644 --- a/packages/spec/src/system/identity.zod.ts +++ b/packages/spec/src/system/identity.zod.ts @@ -1,75 +1,220 @@ import { z } from 'zod'; /** - * Authentication Protocol - * Defines supported authentication standards (OIDC, SAML, LDAP). + * Identity & User Model Specification + * + * Defines the standard user, account, and session data models for ObjectStack. + * These schemas represent "who is logged in" and their associated data. + * + * This is separate from authentication configuration (auth.zod.ts) which + * defines "how to login". */ -export const AuthProtocol = z.enum([ - 'oidc', // OpenID Connect (Modern standard) - 'saml', // SAML 2.0 (Legacy Enterprise) - 'ldap', // LDAP/Active Directory (On-premise) - 'oauth2', // Generic OAuth2 - 'local', // Database username/password - 'mock' // Testing -]); /** - * OIDC / OAuth2 Config (Standard) + * User Schema + * Core user identity data model */ -export const OIDCConfigSchema = z.object({ - issuer: z.string().url().describe('OIDC Issuer URL (.well-known/openid-configuration)'), - clientId: z.string(), - clientSecret: z.string(), // Usually value is ENV reference - scopes: z.array(z.string()).default(['openid', 'profile', 'email']), - attributeMapping: z.record(z.string()).optional().describe('Map IdP claims to User fields'), +export const UserSchema = z.object({ + /** + * Unique user identifier + */ + id: z.string().describe('Unique user identifier'), + + /** + * User's email address (primary identifier) + */ + email: z.string().email().describe('User email address'), + + /** + * Email verification status + */ + emailVerified: z.boolean().default(false).describe('Whether email is verified'), + + /** + * User's display name + */ + name: z.string().optional().describe('User display name'), + + /** + * User's profile image URL + */ + image: z.string().url().optional().describe('Profile image URL'), + + /** + * Account creation timestamp + */ + createdAt: z.date().describe('Account creation timestamp'), + + /** + * Last update timestamp + */ + updatedAt: z.date().describe('Last update timestamp'), }); +export type User = z.infer; + /** - * SAML 2.0 Config (Enterprise) + * Account Schema + * Links external OAuth/OIDC/SAML accounts to a user */ -export const SAMLConfigSchema = z.object({ - entryPoint: z.string().url().describe('IdP SSO URL'), - cert: z.string().describe('IdP Public Certificate'), // PEM format - issuer: z.string().describe('Entity ID of the IdP'), - signatureAlgorithm: z.enum(['sha256', 'sha512']).default('sha256'), - attributeMapping: z.record(z.string()).optional(), +export const AccountSchema = z.object({ + /** + * Unique account identifier + */ + id: z.string().describe('Unique account identifier'), + + /** + * Associated user ID + */ + userId: z.string().describe('Associated user ID'), + + /** + * Account type/provider + */ + type: z.enum([ + 'oauth', + 'oidc', + 'email', + 'credentials', + 'saml', + 'ldap', + ]).describe('Account type'), + + /** + * Provider name (e.g., 'google', 'github', 'okta') + */ + provider: z.string().describe('Provider name'), + + /** + * Provider account ID + */ + providerAccountId: z.string().describe('Provider account ID'), + + /** + * OAuth refresh token + */ + refreshToken: z.string().optional().describe('OAuth refresh token'), + + /** + * OAuth access token + */ + accessToken: z.string().optional().describe('OAuth access token'), + + /** + * Token expiry timestamp + */ + expiresAt: z.number().optional().describe('Token expiry timestamp (Unix)'), + + /** + * OAuth token type + */ + tokenType: z.string().optional().describe('OAuth token type'), + + /** + * OAuth scope + */ + scope: z.string().optional().describe('OAuth scope'), + + /** + * OAuth ID token + */ + idToken: z.string().optional().describe('OAuth ID token'), + + /** + * Session state + */ + sessionState: z.string().optional().describe('Session state'), + + /** + * Account creation timestamp + */ + createdAt: z.date().describe('Account creation timestamp'), + + /** + * Last update timestamp + */ + updatedAt: z.date().describe('Last update timestamp'), }); +export type Account = z.infer; + /** - * LDAP / AD Config (On-premise) + * Session Schema + * User session data model */ -export const LDAPConfigSchema = z.object({ - url: z.string().url().describe('LDAP Server URL (ldap:// or ldaps://)'), - bindDn: z.string(), - bindCredentials: z.string(), - searchBase: z.string(), - searchFilter: z.string(), - groupSearchBase: z.string().optional(), +export const SessionSchema = z.object({ + /** + * Unique session identifier + */ + id: z.string().describe('Unique session identifier'), + + /** + * Session token + */ + sessionToken: z.string().describe('Session token'), + + /** + * Associated user ID + */ + userId: z.string().describe('Associated user ID'), + + /** + * Session expiry timestamp + */ + expires: z.date().describe('Session expiry timestamp'), + + /** + * Session creation timestamp + */ + createdAt: z.date().describe('Session creation timestamp'), + + /** + * Last update timestamp + */ + updatedAt: z.date().describe('Last update timestamp'), + + /** + * IP address of the session + */ + ipAddress: z.string().optional().describe('IP address'), + + /** + * User agent string + */ + userAgent: z.string().optional().describe('User agent string'), + + /** + * Device fingerprint + */ + fingerprint: z.string().optional().describe('Device fingerprint'), }); +export type Session = z.infer; + /** - * Identity Provider (IdP) Schema - * Connects the OS to an external source of truth for identities. + * Verification Token Schema + * Email verification and password reset tokens */ -export const AuthProviderSchema = z.object({ - name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Provider ID'), - label: z.string().describe('Button Label (e.g. "Login with Okta")'), - type: AuthProtocol, - - /** Configuration (Polymorphic based on type) */ - config: z.union([ - OIDCConfigSchema, - SAMLConfigSchema, - LDAPConfigSchema, - z.record(z.any()) // Fallback - ]).describe('Provider specific configuration'), - - /** Visuals */ - icon: z.string().optional().describe('Icon URL or helper class'), +export const VerificationTokenSchema = z.object({ + /** + * Token identifier (email or phone) + */ + identifier: z.string().describe('Token identifier (email or phone)'), + + /** + * Verification token + */ + token: z.string().describe('Verification token'), + + /** + * Token expiry timestamp + */ + expires: z.date().describe('Token expiry timestamp'), - /** Policies */ - active: z.boolean().default(true), - registrationEnabled: z.boolean().default(false).describe('Allow new users to sign up via this provider'), + /** + * Token creation timestamp + */ + createdAt: z.date().describe('Token creation timestamp'), }); -export type AuthProvider = z.infer; +export type VerificationToken = z.infer; From a1a5a58f6feb4cb31a19efe90ceb1097f78a3939 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 09:21:38 +0000 Subject: [PATCH 3/6] Clean up old AuthProtocol and AuthProvider schemas and docs Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../references/misc/identity/AuthProtocol.mdx | 13 -- .../references/misc/identity/AuthProvider.mdx | 16 -- packages/spec/json-schema/AuthProtocol.json | 17 -- packages/spec/json-schema/AuthProvider.json | 171 ------------------ 4 files changed, 217 deletions(-) delete mode 100644 content/docs/references/misc/identity/AuthProtocol.mdx delete mode 100644 content/docs/references/misc/identity/AuthProvider.mdx delete mode 100644 packages/spec/json-schema/AuthProtocol.json delete mode 100644 packages/spec/json-schema/AuthProvider.json diff --git a/content/docs/references/misc/identity/AuthProtocol.mdx b/content/docs/references/misc/identity/AuthProtocol.mdx deleted file mode 100644 index 7f022beb8..000000000 --- a/content/docs/references/misc/identity/AuthProtocol.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: AuthProtocol -description: AuthProtocol Schema Reference ---- - -## Allowed Values - -* `oidc` -* `saml` -* `ldap` -* `oauth2` -* `local` -* `mock` \ No newline at end of file diff --git a/content/docs/references/misc/identity/AuthProvider.mdx b/content/docs/references/misc/identity/AuthProvider.mdx deleted file mode 100644 index d54bb985d..000000000 --- a/content/docs/references/misc/identity/AuthProvider.mdx +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: AuthProvider -description: AuthProvider Schema Reference ---- - -## Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Provider ID | -| **label** | `string` | ✅ | Button Label (e.g. "Login with Okta") | -| **type** | `Enum<'oidc' \| 'saml' \| 'ldap' \| 'oauth2' \| 'local' \| 'mock'>` | ✅ | | -| **config** | `object \| object \| object \| Record` | ✅ | Provider specific configuration | -| **icon** | `string` | optional | Icon URL or helper class | -| **active** | `boolean` | optional | | -| **registrationEnabled** | `boolean` | optional | Allow new users to sign up via this provider | diff --git a/packages/spec/json-schema/AuthProtocol.json b/packages/spec/json-schema/AuthProtocol.json deleted file mode 100644 index da07d2865..000000000 --- a/packages/spec/json-schema/AuthProtocol.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$ref": "#/definitions/AuthProtocol", - "definitions": { - "AuthProtocol": { - "type": "string", - "enum": [ - "oidc", - "saml", - "ldap", - "oauth2", - "local", - "mock" - ] - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/packages/spec/json-schema/AuthProvider.json b/packages/spec/json-schema/AuthProvider.json deleted file mode 100644 index 868885c72..000000000 --- a/packages/spec/json-schema/AuthProvider.json +++ /dev/null @@ -1,171 +0,0 @@ -{ - "$ref": "#/definitions/AuthProvider", - "definitions": { - "AuthProvider": { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "^[a-z_][a-z0-9_]*$", - "description": "Provider ID" - }, - "label": { - "type": "string", - "description": "Button Label (e.g. \"Login with Okta\")" - }, - "type": { - "type": "string", - "enum": [ - "oidc", - "saml", - "ldap", - "oauth2", - "local", - "mock" - ] - }, - "config": { - "anyOf": [ - { - "type": "object", - "properties": { - "issuer": { - "type": "string", - "format": "uri", - "description": "OIDC Issuer URL (.well-known/openid-configuration)" - }, - "clientId": { - "type": "string" - }, - "clientSecret": { - "type": "string" - }, - "scopes": { - "type": "array", - "items": { - "type": "string" - }, - "default": [ - "openid", - "profile", - "email" - ] - }, - "attributeMapping": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Map IdP claims to User fields" - } - }, - "required": [ - "issuer", - "clientId", - "clientSecret" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "entryPoint": { - "type": "string", - "format": "uri", - "description": "IdP SSO URL" - }, - "cert": { - "type": "string", - "description": "IdP Public Certificate" - }, - "issuer": { - "type": "string", - "description": "Entity ID of the IdP" - }, - "signatureAlgorithm": { - "type": "string", - "enum": [ - "sha256", - "sha512" - ], - "default": "sha256" - }, - "attributeMapping": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": [ - "entryPoint", - "cert", - "issuer" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri", - "description": "LDAP Server URL (ldap:// or ldaps://)" - }, - "bindDn": { - "type": "string" - }, - "bindCredentials": { - "type": "string" - }, - "searchBase": { - "type": "string" - }, - "searchFilter": { - "type": "string" - }, - "groupSearchBase": { - "type": "string" - } - }, - "required": [ - "url", - "bindDn", - "bindCredentials", - "searchBase", - "searchFilter" - ], - "additionalProperties": false - }, - { - "type": "object", - "additionalProperties": {} - } - ], - "description": "Provider specific configuration" - }, - "icon": { - "type": "string", - "description": "Icon URL or helper class" - }, - "active": { - "type": "boolean", - "default": true - }, - "registrationEnabled": { - "type": "boolean", - "default": false, - "description": "Allow new users to sign up via this provider" - } - }, - "required": [ - "name", - "label", - "type", - "config" - ], - "additionalProperties": false - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file From 86bac4332f45f1ae1876ab6918a926e6ed0ea095 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 09:22:54 +0000 Subject: [PATCH 4/6] Add comprehensive tests for identity data models (User, Account, Session) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/spec/src/system/identity.test.ts | 281 ++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 packages/spec/src/system/identity.test.ts diff --git a/packages/spec/src/system/identity.test.ts b/packages/spec/src/system/identity.test.ts new file mode 100644 index 000000000..0851c129a --- /dev/null +++ b/packages/spec/src/system/identity.test.ts @@ -0,0 +1,281 @@ +import { describe, it, expect } from 'vitest'; +import { + UserSchema, + AccountSchema, + SessionSchema, + VerificationTokenSchema, + type User, + type Account, + type Session, + type VerificationToken, +} from "./identity.zod"; + +describe('UserSchema', () => { + it('should accept valid user data', () => { + const user: User = { + id: 'user_123', + email: 'test@example.com', + emailVerified: true, + name: 'Test User', + image: 'https://example.com/avatar.jpg', + createdAt: new Date(), + updatedAt: new Date(), + }; + + expect(() => UserSchema.parse(user)).not.toThrow(); + }); + + it('should accept minimal user data', () => { + const user = { + id: 'user_123', + email: 'test@example.com', + createdAt: new Date(), + updatedAt: new Date(), + }; + + const result = UserSchema.parse(user); + expect(result.emailVerified).toBe(false); // default value + }); + + it('should validate email format', () => { + const user = { + id: 'user_123', + email: 'invalid-email', + createdAt: new Date(), + updatedAt: new Date(), + }; + + expect(() => UserSchema.parse(user)).toThrow(); + }); + + it('should validate image URL format', () => { + const user = { + id: 'user_123', + email: 'test@example.com', + image: 'not-a-url', + createdAt: new Date(), + updatedAt: new Date(), + }; + + expect(() => UserSchema.parse(user)).toThrow(); + }); + + it('should accept user without optional fields', () => { + const user = { + id: 'user_123', + email: 'test@example.com', + createdAt: new Date(), + updatedAt: new Date(), + }; + + expect(() => UserSchema.parse(user)).not.toThrow(); + }); +}); + +describe('AccountSchema', () => { + it('should accept valid OAuth account', () => { + const account: Account = { + id: 'account_123', + userId: 'user_123', + type: 'oauth', + provider: 'google', + providerAccountId: 'google_user_123', + accessToken: 'access_token_xyz', + refreshToken: 'refresh_token_xyz', + expiresAt: Date.now() + 3600000, + tokenType: 'Bearer', + scope: 'openid profile email', + createdAt: new Date(), + updatedAt: new Date(), + }; + + expect(() => AccountSchema.parse(account)).not.toThrow(); + }); + + it('should accept minimal account data', () => { + const account = { + id: 'account_123', + userId: 'user_123', + type: 'email', + provider: 'email', + providerAccountId: 'email_user_123', + createdAt: new Date(), + updatedAt: new Date(), + }; + + expect(() => AccountSchema.parse(account)).not.toThrow(); + }); + + it('should accept all account types', () => { + const types = ['oauth', 'oidc', 'email', 'credentials', 'saml', 'ldap'] as const; + + types.forEach((type) => { + const account = { + id: 'account_123', + userId: 'user_123', + type, + provider: 'provider', + providerAccountId: 'provider_account_123', + createdAt: new Date(), + updatedAt: new Date(), + }; + expect(() => AccountSchema.parse(account)).not.toThrow(); + }); + }); + + it('should reject invalid account type', () => { + const account = { + id: 'account_123', + userId: 'user_123', + type: 'invalid', + provider: 'provider', + providerAccountId: 'provider_account_123', + createdAt: new Date(), + updatedAt: new Date(), + }; + + expect(() => AccountSchema.parse(account)).toThrow(); + }); +}); + +describe('SessionSchema', () => { + it('should accept valid session data', () => { + const session: Session = { + id: 'session_123', + sessionToken: 'session_token_xyz', + userId: 'user_123', + expires: new Date(Date.now() + 86400000), + createdAt: new Date(), + updatedAt: new Date(), + ipAddress: '192.168.1.1', + userAgent: 'Mozilla/5.0', + fingerprint: 'fingerprint_xyz', + }; + + expect(() => SessionSchema.parse(session)).not.toThrow(); + }); + + it('should accept minimal session data', () => { + const session = { + id: 'session_123', + sessionToken: 'session_token_xyz', + userId: 'user_123', + expires: new Date(), + createdAt: new Date(), + updatedAt: new Date(), + }; + + expect(() => SessionSchema.parse(session)).not.toThrow(); + }); + + it('should accept session with device information', () => { + const session = { + id: 'session_123', + sessionToken: 'session_token_xyz', + userId: 'user_123', + expires: new Date(), + createdAt: new Date(), + updatedAt: new Date(), + ipAddress: '10.0.0.1', + userAgent: 'Chrome/120.0.0.0', + fingerprint: 'device_fingerprint', + }; + + expect(() => SessionSchema.parse(session)).not.toThrow(); + }); +}); + +describe('VerificationTokenSchema', () => { + it('should accept valid verification token', () => { + const token: VerificationToken = { + identifier: 'test@example.com', + token: 'verification_token_xyz', + expires: new Date(Date.now() + 3600000), + createdAt: new Date(), + }; + + expect(() => VerificationTokenSchema.parse(token)).not.toThrow(); + }); + + it('should accept token with phone identifier', () => { + const token = { + identifier: '+1234567890', + token: 'verification_token_xyz', + expires: new Date(), + createdAt: new Date(), + }; + + expect(() => VerificationTokenSchema.parse(token)).not.toThrow(); + }); + + it('should accept token with email identifier', () => { + const token = { + identifier: 'user@example.com', + token: 'reset_password_token', + expires: new Date(), + createdAt: new Date(), + }; + + expect(() => VerificationTokenSchema.parse(token)).not.toThrow(); + }); +}); + +describe('Type inference', () => { + it('should correctly infer User type', () => { + const user: User = { + id: 'user_123', + email: 'test@example.com', + emailVerified: true, + createdAt: new Date(), + updatedAt: new Date(), + }; + + // This test passes if TypeScript compiles without errors + expect(user.id).toBe('user_123'); + expect(user.email).toBe('test@example.com'); + }); + + it('should correctly infer Account type', () => { + const account: Account = { + id: 'account_123', + userId: 'user_123', + type: 'oauth', + provider: 'google', + providerAccountId: 'google_123', + createdAt: new Date(), + updatedAt: new Date(), + }; + + // This test passes if TypeScript compiles without errors + expect(account.type).toBe('oauth'); + expect(account.provider).toBe('google'); + }); + + it('should correctly infer Session type', () => { + const session: Session = { + id: 'session_123', + sessionToken: 'token_xyz', + userId: 'user_123', + expires: new Date(), + createdAt: new Date(), + updatedAt: new Date(), + }; + + // This test passes if TypeScript compiles without errors + expect(session.id).toBe('session_123'); + expect(session.userId).toBe('user_123'); + }); + + it('should correctly infer VerificationToken type', () => { + const token: VerificationToken = { + identifier: 'test@example.com', + token: 'token_xyz', + expires: new Date(), + createdAt: new Date(), + }; + + // This test passes if TypeScript compiles without errors + expect(token.identifier).toBe('test@example.com'); + expect(token.token).toBe('token_xyz'); + }); +}); From fb0b264180cdf9f698b1016cab1c82ffcfe66ca7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:54:41 +0000 Subject: [PATCH 5/6] Add database field mapping for driver compatibility (better-auth support) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- content/docs/references/system/AuthConfig.mdx | 1 + .../references/system/DatabaseMapping.mdx | 13 ++ docs/AUTHENTICATION_STANDARD.md | 82 ++++++++++ packages/spec/json-schema/AuthConfig.json | 42 +++++ .../spec/json-schema/DatabaseMapping.json | 48 ++++++ .../json-schema/StandardAuthProvider.json | 42 +++++ packages/spec/src/system/auth.test.ts | 149 ++++++++++++++++++ packages/spec/src/system/auth.zod.ts | 66 ++++++++ 8 files changed, 443 insertions(+) create mode 100644 content/docs/references/system/DatabaseMapping.mdx create mode 100644 packages/spec/json-schema/DatabaseMapping.json diff --git a/content/docs/references/system/AuthConfig.mdx b/content/docs/references/system/AuthConfig.mdx index 05f40eb3b..514a25cee 100644 --- a/content/docs/references/system/AuthConfig.mdx +++ b/content/docs/references/system/AuthConfig.mdx @@ -25,6 +25,7 @@ description: AuthConfig Schema Reference | **enterprise** | `object` | optional | | | **userFieldMapping** | `object` | optional | | | **database** | `object` | optional | | +| **mapping** | `object` | optional | | | **plugins** | `object[]` | optional | | | **hooks** | `object` | optional | Authentication lifecycle hooks | | **security** | `object` | optional | Advanced security settings | diff --git a/content/docs/references/system/DatabaseMapping.mdx b/content/docs/references/system/DatabaseMapping.mdx new file mode 100644 index 000000000..b618a0b34 --- /dev/null +++ b/content/docs/references/system/DatabaseMapping.mdx @@ -0,0 +1,13 @@ +--- +title: DatabaseMapping +description: DatabaseMapping Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **user** | `Record` | optional | User field mapping (e.g., `{ "emailVerified": "email_verified" }`) | +| **session** | `Record` | optional | Session field mapping | +| **account** | `Record` | optional | Account field mapping | +| **verificationToken** | `Record` | optional | VerificationToken field mapping | diff --git a/docs/AUTHENTICATION_STANDARD.md b/docs/AUTHENTICATION_STANDARD.md index 04be91da8..735d36250 100644 --- a/docs/AUTHENTICATION_STANDARD.md +++ b/docs/AUTHENTICATION_STANDARD.md @@ -416,6 +416,88 @@ hooks: { } ``` +### Database Field Mapping + +The `mapping` configuration allows you to map ObjectStack standard field names (which follow Auth.js conventions) to driver-specific field names. This is particularly useful for ensuring compatibility with authentication libraries like `better-auth` that use different column naming conventions. + +```typescript +mapping: { + // User model field mapping (optional) + user?: { + emailVerified: 'email_verified', // Map to snake_case + createdAt: 'created_at', + updatedAt: 'updated_at', + }, + + // Session model field mapping (default better-auth compatible) + session: { + sessionToken: 'token', // better-auth uses 'token' + expires: 'expiresAt', // better-auth uses 'expiresAt' + }, + + // Account model field mapping (default better-auth compatible) + account: { + providerAccountId: 'accountId', // better-auth uses 'accountId' + provider: 'providerId', // better-auth uses 'providerId' + }, + + // Verification token field mapping (optional) + verificationToken?: { + identifier: 'email', + }, +} +``` + +**Default Mappings for better-auth Compatibility:** + +By default, the `session` and `account` mappings are pre-configured for `better-auth` compatibility: + +| ObjectStack Field | better-auth Field | +|------------------|------------------| +| `sessionToken` | `token` | +| `expires` | `expiresAt` | +| `providerAccountId` | `accountId` | +| `provider` | `providerId` | + +You only need to specify the `mapping` configuration if you want to: +- Use a different authentication driver with non-standard field names +- Override the default better-auth mappings +- Add custom mappings for user or verification token fields + +**Example with custom mappings:** + +```typescript +const authConfig: AuthConfig = { + name: 'custom_auth', + label: 'Custom Auth', + driver: 'custom-driver', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: process.env.AUTH_SECRET!, + + mapping: { + user: { + emailVerified: 'is_verified', + createdAt: 'created', + updatedAt: 'modified', + }, + session: { + sessionToken: 'session_id', + expires: 'expires_at', + }, + account: { + providerAccountId: 'external_id', + provider: 'auth_provider', + }, + }, + + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, +}; +``` + ## Supported OAuth Providers - Google (`google`) diff --git a/packages/spec/json-schema/AuthConfig.json b/packages/spec/json-schema/AuthConfig.json index 94d7ff243..56a131403 100644 --- a/packages/spec/json-schema/AuthConfig.json +++ b/packages/spec/json-schema/AuthConfig.json @@ -621,6 +621,48 @@ ], "additionalProperties": false }, + "mapping": { + "type": "object", + "properties": { + "user": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "User field mapping (e.g., { \"emailVerified\": \"email_verified\" })" + }, + "session": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": { + "sessionToken": "token", + "expires": "expiresAt" + }, + "description": "Session field mapping" + }, + "account": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": { + "providerAccountId": "accountId", + "provider": "providerId" + }, + "description": "Account field mapping" + }, + "verificationToken": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "VerificationToken field mapping" + } + }, + "additionalProperties": false + }, "plugins": { "type": "array", "items": { diff --git a/packages/spec/json-schema/DatabaseMapping.json b/packages/spec/json-schema/DatabaseMapping.json new file mode 100644 index 000000000..9210807fb --- /dev/null +++ b/packages/spec/json-schema/DatabaseMapping.json @@ -0,0 +1,48 @@ +{ + "$ref": "#/definitions/DatabaseMapping", + "definitions": { + "DatabaseMapping": { + "type": "object", + "properties": { + "user": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "User field mapping (e.g., { \"emailVerified\": \"email_verified\" })" + }, + "session": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": { + "sessionToken": "token", + "expires": "expiresAt" + }, + "description": "Session field mapping" + }, + "account": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": { + "providerAccountId": "accountId", + "provider": "providerId" + }, + "description": "Account field mapping" + }, + "verificationToken": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "VerificationToken field mapping" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/StandardAuthProvider.json b/packages/spec/json-schema/StandardAuthProvider.json index f34b0d5ad..12b41ef5f 100644 --- a/packages/spec/json-schema/StandardAuthProvider.json +++ b/packages/spec/json-schema/StandardAuthProvider.json @@ -629,6 +629,48 @@ ], "additionalProperties": false }, + "mapping": { + "type": "object", + "properties": { + "user": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "User field mapping (e.g., { \"emailVerified\": \"email_verified\" })" + }, + "session": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": { + "sessionToken": "token", + "expires": "expiresAt" + }, + "description": "Session field mapping" + }, + "account": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": { + "providerAccountId": "accountId", + "provider": "providerId" + }, + "description": "Account field mapping" + }, + "verificationToken": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "VerificationToken field mapping" + } + }, + "additionalProperties": false + }, "plugins": { "type": "array", "items": { diff --git a/packages/spec/src/system/auth.test.ts b/packages/spec/src/system/auth.test.ts index 9968273f5..4c4fc40f9 100644 --- a/packages/spec/src/system/auth.test.ts +++ b/packages/spec/src/system/auth.test.ts @@ -16,12 +16,14 @@ import { EnterpriseAuthConfigSchema, UserFieldMappingSchema, DatabaseAdapterSchema, + DatabaseMappingSchema, AuthPluginConfigSchema, AuthConfigSchema, StandardAuthProviderSchema, type AuthConfig, type StandardAuthProvider, type OAuthProvider, + type DatabaseMapping, } from "./auth.zod"; describe('AuthStrategy', () => { @@ -544,6 +546,93 @@ describe('DatabaseAdapterSchema', () => { }); }); +describe('DatabaseMappingSchema', () => { + it('should accept valid database mapping configuration', () => { + const mapping: DatabaseMapping = { + user: { + emailVerified: 'email_verified', + createdAt: 'created_at', + }, + session: { + sessionToken: 'token', + expires: 'expiresAt', + }, + account: { + providerAccountId: 'accountId', + provider: 'providerId', + }, + verificationToken: { + identifier: 'email', + }, + }; + + expect(() => DatabaseMappingSchema.parse(mapping)).not.toThrow(); + }); + + it('should use default mappings for session and account', () => { + const mapping = {}; + const result = DatabaseMappingSchema.parse(mapping); + + // Session defaults + expect(result.session.sessionToken).toBe('token'); + expect(result.session.expires).toBe('expiresAt'); + + // Account defaults + expect(result.account.providerAccountId).toBe('accountId'); + expect(result.account.provider).toBe('providerId'); + }); + + it('should accept partial mapping overrides', () => { + const mapping = { + session: { + sessionToken: 'session_token', + // Let expires use default + }, + account: { + // Override only one field + providerAccountId: 'provider_account_id', + }, + }; + + const result = DatabaseMappingSchema.parse(mapping); + expect(result.session.sessionToken).toBe('session_token'); + expect(result.account.providerAccountId).toBe('provider_account_id'); + }); + + it('should accept custom user field mappings', () => { + const mapping = { + user: { + emailVerified: 'is_verified', + createdAt: 'created', + updatedAt: 'modified', + }, + }; + + expect(() => DatabaseMappingSchema.parse(mapping)).not.toThrow(); + }); + + it('should handle better-auth compatibility mappings', () => { + const betterAuthMapping = { + session: { + sessionToken: 'token', + expires: 'expiresAt', + }, + account: { + providerAccountId: 'accountId', + provider: 'providerId', + }, + }; + + const result = DatabaseMappingSchema.parse(betterAuthMapping); + + // Verify better-auth compatible mappings + expect(result.session.sessionToken).toBe('token'); + expect(result.session.expires).toBe('expiresAt'); + expect(result.account.providerAccountId).toBe('accountId'); + expect(result.account.provider).toBe('providerId'); + }); +}); + describe('AuthPluginConfigSchema', () => { it('should accept valid plugin configuration', () => { const config = { @@ -855,6 +944,66 @@ describe('AuthConfigSchema', () => { expect(() => AuthConfigSchema.parse(config)).not.toThrow(); }); + it('should accept configuration with database field mapping', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + mapping: { + user: { + emailVerified: 'email_verified', + }, + session: { + sessionToken: 'token', + expires: 'expiresAt', + }, + account: { + providerAccountId: 'accountId', + provider: 'providerId', + }, + }, + }; + + expect(() => AuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept configuration with better-auth compatible mapping', () => { + const config = { + name: 'better_auth_config', + label: 'Better Auth Compatible', + driver: 'better-auth', + strategies: ['email_password', 'oauth'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + mapping: { + session: { + sessionToken: 'token', + expires: 'expiresAt', + }, + account: { + providerAccountId: 'accountId', + provider: 'providerId', + }, + }, + }; + + const result = AuthConfigSchema.parse(config); + + // Verify mapping is preserved + expect(result.mapping?.session?.sessionToken).toBe('token'); + expect(result.mapping?.account?.providerAccountId).toBe('accountId'); + }); + it('should use default values for optional fields', () => { const config = { name: 'test_auth', diff --git a/packages/spec/src/system/auth.zod.ts b/packages/spec/src/system/auth.zod.ts index b86d71e15..c5f587b6b 100644 --- a/packages/spec/src/system/auth.zod.ts +++ b/packages/spec/src/system/auth.zod.ts @@ -324,6 +324,62 @@ export const DatabaseAdapterSchema = z.object({ export type DatabaseAdapter = z.infer; +/** + * Database Field Mapping Configuration + * Maps ObjectStack standard field names to driver-specific field names. + * + * Useful when the underlying authentication driver (e.g., better-auth) uses + * different column names than the ObjectStack standard schemas (which follow + * Auth.js conventions). + * + * @example + * ```typescript + * mapping: { + * session: { + * sessionToken: 'token', // better-auth uses 'token' + * expires: 'expiresAt' // better-auth uses 'expiresAt' + * }, + * account: { + * providerAccountId: 'accountId', // better-auth uses 'accountId' + * provider: 'providerId' // better-auth uses 'providerId' + * } + * } + * ``` + */ +export const DatabaseMappingSchema = z.object({ + /** + * User model field mapping + * Maps ObjectStack User fields to driver fields + */ + user: z.record(z.string()).optional().describe('User field mapping (e.g., { "emailVerified": "email_verified" })'), + + /** + * Session model field mapping + * Maps ObjectStack Session fields to driver fields + */ + session: z.record(z.string()).default({ + sessionToken: 'token', + expires: 'expiresAt', + }).describe('Session field mapping'), + + /** + * Account model field mapping + * Maps ObjectStack Account fields to driver fields + */ + account: z.record(z.string()).default({ + providerAccountId: 'accountId', + provider: 'providerId', + }).describe('Account field mapping'), + + /** + * Verification token field mapping + * Maps ObjectStack VerificationToken fields to driver fields + */ + verificationToken: z.record(z.string()).optional().describe('VerificationToken field mapping'), +}); + +export type DatabaseMapping = z.infer; + /** * Authentication Plugin Configuration * Extends authentication with additional features @@ -469,6 +525,16 @@ export const AuthConfigSchema = z.object({ */ database: DatabaseAdapterSchema.optional(), + /** + * Database field mapping configuration + * Maps ObjectStack standard field names to driver-specific field names. + * + * This is distinct from the database adapter configuration and provides + * instructions for the driver to map our standard schema fields to the + * underlying engine's fields (e.g., better-auth uses 'token' instead of 'sessionToken'). + */ + mapping: DatabaseMappingSchema.optional(), + /** * Additional authentication plugins */ From 3dd6ea561a9f1f6c9fa789aac2dcef043c5c0dec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:58:17 +0000 Subject: [PATCH 6/6] Extract better-auth field mapping defaults into constants for maintainability Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/spec/src/system/auth.zod.ts | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/spec/src/system/auth.zod.ts b/packages/spec/src/system/auth.zod.ts index c5f587b6b..a45170ad0 100644 --- a/packages/spec/src/system/auth.zod.ts +++ b/packages/spec/src/system/auth.zod.ts @@ -324,6 +324,22 @@ export const DatabaseAdapterSchema = z.object({ export type DatabaseAdapter = z.infer; +/** + * Default field mappings for better-auth compatibility + * These mappings bridge the gap between ObjectStack standard (Auth.js conventions) + * and better-auth's field naming conventions + */ +export const BETTER_AUTH_FIELD_MAPPINGS = { + session: { + sessionToken: 'token', + expires: 'expiresAt', + }, + account: { + providerAccountId: 'accountId', + provider: 'providerId', + }, +} as const; + /** * Database Field Mapping Configuration * Maps ObjectStack standard field names to driver-specific field names. @@ -357,19 +373,13 @@ export const DatabaseMappingSchema = z.object({ * Session model field mapping * Maps ObjectStack Session fields to driver fields */ - session: z.record(z.string()).default({ - sessionToken: 'token', - expires: 'expiresAt', - }).describe('Session field mapping'), + session: z.record(z.string()).default(BETTER_AUTH_FIELD_MAPPINGS.session).describe('Session field mapping'), /** * Account model field mapping * Maps ObjectStack Account fields to driver fields */ - account: z.record(z.string()).default({ - providerAccountId: 'accountId', - provider: 'providerId', - }).describe('Account field mapping'), + account: z.record(z.string()).default(BETTER_AUTH_FIELD_MAPPINGS.account).describe('Account field mapping'), /** * Verification token field mapping