diff --git a/content/docs/references/misc/AuthenticationConfig.mdx b/content/docs/references/misc/AuthenticationConfig.mdx new file mode 100644 index 000000000..eb7e2f280 --- /dev/null +++ b/content/docs/references/misc/AuthenticationConfig.mdx @@ -0,0 +1,32 @@ +--- +title: AuthenticationConfig +description: AuthenticationConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Configuration name (snake_case) | +| **label** | `string` | ✅ | Display label | +| **strategies** | `Enum<'email_password' \| 'magic_link' \| 'oauth' \| 'passkey' \| 'otp' \| 'anonymous'>[]` | ✅ | Enabled authentication strategies | +| **baseUrl** | `string` | ✅ | Application base URL | +| **secret** | `string` | ✅ | Secret key for signing (min 32 chars) | +| **emailPassword** | `object` | optional | | +| **magicLink** | `object` | optional | | +| **passkey** | `object` | optional | | +| **oauth** | `object` | optional | | +| **session** | `object` | optional | | +| **rateLimit** | `object` | optional | | +| **csrf** | `object` | optional | | +| **accountLinking** | `object` | optional | | +| **twoFactor** | `object` | optional | | +| **userFieldMapping** | `object` | optional | | +| **database** | `object` | optional | | +| **plugins** | `object[]` | optional | | +| **hooks** | `object` | optional | Authentication lifecycle hooks | +| **security** | `object` | optional | Advanced security settings | +| **email** | `object` | optional | Email configuration | +| **ui** | `object` | optional | UI customization | +| **active** | `boolean` | optional | Whether this provider is active | +| **allowRegistration** | `boolean` | optional | Allow new user registration | diff --git a/content/docs/references/misc/AuthenticationProvider.mdx b/content/docs/references/misc/AuthenticationProvider.mdx new file mode 100644 index 000000000..e5cc9803b --- /dev/null +++ b/content/docs/references/misc/AuthenticationProvider.mdx @@ -0,0 +1,11 @@ +--- +title: AuthenticationProvider +description: AuthenticationProvider Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Provider type identifier | +| **config** | `object` | ✅ | Authentication configuration | diff --git a/content/docs/references/system/AccountLinkingConfig.mdx b/content/docs/references/system/AccountLinkingConfig.mdx new file mode 100644 index 000000000..df6148efd --- /dev/null +++ b/content/docs/references/system/AccountLinkingConfig.mdx @@ -0,0 +1,12 @@ +--- +title: AccountLinkingConfig +description: AccountLinkingConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | Allow account linking | +| **autoLink** | `boolean` | optional | Automatically link accounts with same email | +| **requireVerification** | `boolean` | optional | Require email verification before linking | diff --git a/content/docs/references/system/AuthConfig.mdx b/content/docs/references/system/AuthConfig.mdx new file mode 100644 index 000000000..1f7c710da --- /dev/null +++ b/content/docs/references/system/AuthConfig.mdx @@ -0,0 +1,33 @@ +--- +title: AuthConfig +description: AuthConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Configuration name (snake_case) | +| **label** | `string` | ✅ | Display label | +| **driver** | `string` | optional | The underlying authentication implementation driver | +| **strategies** | `Enum<'email_password' \| 'magic_link' \| 'oauth' \| 'passkey' \| 'otp' \| 'anonymous'>[]` | ✅ | Enabled authentication strategies | +| **baseUrl** | `string` | ✅ | Application base URL | +| **secret** | `string` | ✅ | Secret key for signing (min 32 chars) | +| **emailPassword** | `object` | optional | | +| **magicLink** | `object` | optional | | +| **passkey** | `object` | optional | | +| **oauth** | `object` | optional | | +| **session** | `object` | optional | | +| **rateLimit** | `object` | optional | | +| **csrf** | `object` | optional | | +| **accountLinking** | `object` | optional | | +| **twoFactor** | `object` | optional | | +| **userFieldMapping** | `object` | optional | | +| **database** | `object` | optional | | +| **plugins** | `object[]` | optional | | +| **hooks** | `object` | optional | Authentication lifecycle hooks | +| **security** | `object` | optional | Advanced security settings | +| **email** | `object` | optional | Email configuration | +| **ui** | `object` | optional | UI customization | +| **active** | `boolean` | optional | Whether this provider is active | +| **allowRegistration** | `boolean` | optional | Allow new user registration | diff --git a/content/docs/references/system/AuthPluginConfig.mdx b/content/docs/references/system/AuthPluginConfig.mdx new file mode 100644 index 000000000..701df2e7b --- /dev/null +++ b/content/docs/references/system/AuthPluginConfig.mdx @@ -0,0 +1,12 @@ +--- +title: AuthPluginConfig +description: AuthPluginConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Plugin name | +| **enabled** | `boolean` | optional | | +| **options** | `Record` | optional | Plugin-specific options | diff --git a/content/docs/references/system/AuthStrategy.mdx b/content/docs/references/system/AuthStrategy.mdx new file mode 100644 index 000000000..9baafb1ad --- /dev/null +++ b/content/docs/references/system/AuthStrategy.mdx @@ -0,0 +1,13 @@ +--- +title: AuthStrategy +description: AuthStrategy Schema Reference +--- + +## Allowed Values + +* `email_password` +* `magic_link` +* `oauth` +* `passkey` +* `otp` +* `anonymous` \ No newline at end of file diff --git a/content/docs/references/system/AuthenticationConfig.mdx b/content/docs/references/system/AuthenticationConfig.mdx new file mode 100644 index 000000000..eb7e2f280 --- /dev/null +++ b/content/docs/references/system/AuthenticationConfig.mdx @@ -0,0 +1,32 @@ +--- +title: AuthenticationConfig +description: AuthenticationConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Configuration name (snake_case) | +| **label** | `string` | ✅ | Display label | +| **strategies** | `Enum<'email_password' \| 'magic_link' \| 'oauth' \| 'passkey' \| 'otp' \| 'anonymous'>[]` | ✅ | Enabled authentication strategies | +| **baseUrl** | `string` | ✅ | Application base URL | +| **secret** | `string` | ✅ | Secret key for signing (min 32 chars) | +| **emailPassword** | `object` | optional | | +| **magicLink** | `object` | optional | | +| **passkey** | `object` | optional | | +| **oauth** | `object` | optional | | +| **session** | `object` | optional | | +| **rateLimit** | `object` | optional | | +| **csrf** | `object` | optional | | +| **accountLinking** | `object` | optional | | +| **twoFactor** | `object` | optional | | +| **userFieldMapping** | `object` | optional | | +| **database** | `object` | optional | | +| **plugins** | `object[]` | optional | | +| **hooks** | `object` | optional | Authentication lifecycle hooks | +| **security** | `object` | optional | Advanced security settings | +| **email** | `object` | optional | Email configuration | +| **ui** | `object` | optional | UI customization | +| **active** | `boolean` | optional | Whether this provider is active | +| **allowRegistration** | `boolean` | optional | Allow new user registration | diff --git a/content/docs/references/system/AuthenticationProvider.mdx b/content/docs/references/system/AuthenticationProvider.mdx new file mode 100644 index 000000000..e5cc9803b --- /dev/null +++ b/content/docs/references/system/AuthenticationProvider.mdx @@ -0,0 +1,11 @@ +--- +title: AuthenticationProvider +description: AuthenticationProvider Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Provider type identifier | +| **config** | `object` | ✅ | Authentication configuration | diff --git a/content/docs/references/system/CSRFConfig.mdx b/content/docs/references/system/CSRFConfig.mdx new file mode 100644 index 000000000..eba2dddf6 --- /dev/null +++ b/content/docs/references/system/CSRFConfig.mdx @@ -0,0 +1,13 @@ +--- +title: CSRFConfig +description: CSRFConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **tokenLength** | `number` | optional | CSRF token length | +| **cookieName** | `string` | optional | CSRF cookie name | +| **headerName** | `string` | optional | CSRF header name | diff --git a/content/docs/references/system/DatabaseAdapter.mdx b/content/docs/references/system/DatabaseAdapter.mdx new file mode 100644 index 000000000..804f75b03 --- /dev/null +++ b/content/docs/references/system/DatabaseAdapter.mdx @@ -0,0 +1,13 @@ +--- +title: DatabaseAdapter +description: DatabaseAdapter Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'prisma' \| 'drizzle' \| 'kysely' \| 'custom'>` | ✅ | Database adapter type | +| **connectionString** | `string` | optional | Database connection string | +| **tablePrefix** | `string` | optional | Prefix for auth tables | +| **schema** | `string` | optional | Database schema name | diff --git a/content/docs/references/system/EmailPasswordConfig.mdx b/content/docs/references/system/EmailPasswordConfig.mdx new file mode 100644 index 000000000..ed35be72f --- /dev/null +++ b/content/docs/references/system/EmailPasswordConfig.mdx @@ -0,0 +1,15 @@ +--- +title: EmailPasswordConfig +description: EmailPasswordConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **requireEmailVerification** | `boolean` | optional | Require email verification before login | +| **minPasswordLength** | `number` | optional | Minimum password length | +| **requirePasswordComplexity** | `boolean` | optional | Require uppercase, lowercase, numbers, symbols | +| **allowPasswordReset** | `boolean` | optional | Enable password reset functionality | +| **passwordResetExpiry** | `number` | optional | Password reset token expiry in seconds | diff --git a/content/docs/references/system/MagicLinkConfig.mdx b/content/docs/references/system/MagicLinkConfig.mdx new file mode 100644 index 000000000..2971b2862 --- /dev/null +++ b/content/docs/references/system/MagicLinkConfig.mdx @@ -0,0 +1,11 @@ +--- +title: MagicLinkConfig +description: MagicLinkConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **expiryTime** | `number` | optional | Magic link expiry time in seconds (default 15 min) | diff --git a/content/docs/references/system/OAuthProvider.mdx b/content/docs/references/system/OAuthProvider.mdx new file mode 100644 index 000000000..32ff86e83 --- /dev/null +++ b/content/docs/references/system/OAuthProvider.mdx @@ -0,0 +1,17 @@ +--- +title: OAuthProvider +description: OAuthProvider Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **provider** | `Enum<'google' \| 'github' \| 'facebook' \| 'twitter' \| 'linkedin' \| 'microsoft' \| 'apple' \| 'discord' \| 'gitlab' \| 'custom'>` | ✅ | OAuth provider type | +| **clientId** | `string` | ✅ | OAuth client ID | +| **clientSecret** | `string` | ✅ | OAuth client secret (typically from ENV) | +| **scopes** | `string[]` | optional | Requested OAuth scopes | +| **redirectUri** | `string` | optional | OAuth callback URL | +| **enabled** | `boolean` | optional | Whether this provider is enabled | +| **displayName** | `string` | optional | Display name for the provider button | +| **icon** | `string` | optional | Icon URL or identifier | diff --git a/content/docs/references/system/PasskeyConfig.mdx b/content/docs/references/system/PasskeyConfig.mdx new file mode 100644 index 000000000..9e305ed4c --- /dev/null +++ b/content/docs/references/system/PasskeyConfig.mdx @@ -0,0 +1,15 @@ +--- +title: PasskeyConfig +description: PasskeyConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **rpName** | `string` | ✅ | Relying Party name | +| **rpId** | `string` | optional | Relying Party ID (defaults to domain) | +| **allowedOrigins** | `string[]` | optional | Allowed origins for WebAuthn | +| **userVerification** | `Enum<'required' \| 'preferred' \| 'discouraged'>` | optional | | +| **attestation** | `Enum<'none' \| 'indirect' \| 'direct' \| 'enterprise'>` | optional | | diff --git a/content/docs/references/system/RateLimitConfig.mdx b/content/docs/references/system/RateLimitConfig.mdx new file mode 100644 index 000000000..6682505c5 --- /dev/null +++ b/content/docs/references/system/RateLimitConfig.mdx @@ -0,0 +1,14 @@ +--- +title: RateLimitConfig +description: RateLimitConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **maxAttempts** | `number` | optional | Maximum login attempts | +| **windowMs** | `number` | optional | Time window in milliseconds (default 15 min) | +| **blockDuration** | `number` | optional | Block duration after max attempts in ms | +| **skipSuccessfulRequests** | `boolean` | optional | Only count failed requests | diff --git a/content/docs/references/system/SessionConfig.mdx b/content/docs/references/system/SessionConfig.mdx new file mode 100644 index 000000000..5fb4b1c25 --- /dev/null +++ b/content/docs/references/system/SessionConfig.mdx @@ -0,0 +1,17 @@ +--- +title: SessionConfig +description: SessionConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **expiresIn** | `number` | optional | Session expiry in seconds (default 7 days) | +| **updateAge** | `number` | optional | Session update interval in seconds (default 1 day) | +| **cookieName** | `string` | optional | Session cookie name | +| **cookieSecure** | `boolean` | optional | Use secure cookies (HTTPS only) | +| **cookieSameSite** | `Enum<'strict' \| 'lax' \| 'none'>` | optional | SameSite cookie attribute | +| **cookieDomain** | `string` | optional | Cookie domain | +| **cookiePath** | `string` | optional | Cookie path | +| **cookieHttpOnly** | `boolean` | optional | HttpOnly cookie attribute | diff --git a/content/docs/references/system/StandardAuthProvider.mdx b/content/docs/references/system/StandardAuthProvider.mdx new file mode 100644 index 000000000..fdc46136f --- /dev/null +++ b/content/docs/references/system/StandardAuthProvider.mdx @@ -0,0 +1,11 @@ +--- +title: StandardAuthProvider +description: StandardAuthProvider Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Provider type identifier | +| **config** | `object` | ✅ | Standard authentication configuration | diff --git a/content/docs/references/system/TwoFactorConfig.mdx b/content/docs/references/system/TwoFactorConfig.mdx new file mode 100644 index 000000000..70add73db --- /dev/null +++ b/content/docs/references/system/TwoFactorConfig.mdx @@ -0,0 +1,13 @@ +--- +title: TwoFactorConfig +description: TwoFactorConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **enabled** | `boolean` | optional | | +| **issuer** | `string` | optional | TOTP issuer name | +| **qrCodeSize** | `number` | optional | QR code size in pixels | +| **backupCodes** | `object` | optional | | diff --git a/content/docs/references/system/UserFieldMapping.mdx b/content/docs/references/system/UserFieldMapping.mdx new file mode 100644 index 000000000..bfbf94d14 --- /dev/null +++ b/content/docs/references/system/UserFieldMapping.mdx @@ -0,0 +1,16 @@ +--- +title: UserFieldMapping +description: UserFieldMapping Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | optional | User ID field | +| **email** | `string` | optional | Email field | +| **name** | `string` | optional | Name field | +| **image** | `string` | optional | Profile image field | +| **emailVerified** | `string` | optional | Email verification status field | +| **createdAt** | `string` | optional | Created timestamp field | +| **updatedAt** | `string` | optional | Updated timestamp field | diff --git a/docs/AUTHENTICATION_STANDARD.md b/docs/AUTHENTICATION_STANDARD.md new file mode 100644 index 000000000..0c7582c8b --- /dev/null +++ b/docs/AUTHENTICATION_STANDARD.md @@ -0,0 +1,375 @@ +# ObjectStack Authentication Standard + +The standard authentication protocol specification for the ObjectStack ecosystem. + +## Overview + +This document defines the **ObjectStack Authentication Standard**, a comprehensive, framework-agnostic authentication protocol for ObjectStack applications. This standard supports multiple authentication strategies, session management, and comprehensive security features. + +The specification is designed as an **interface** that can be implemented by any authentication library. **better-auth** serves as the **Reference Implementation** (default driver) for this standard. + +### Implementation Drivers + +The authentication standard can be implemented using various drivers: +- **better-auth** (default/reference implementation) +- Auth.js +- Passport +- Custom implementations + +## Features + +### Authentication Strategies + +- **Email/Password**: Traditional email and password authentication with customizable password policies +- **Magic Link**: Passwordless email-based authentication +- **OAuth**: Social login with popular providers (Google, GitHub, Facebook, Twitter, LinkedIn, Microsoft, Apple, Discord, GitLab) +- **Passkey**: WebAuthn/FIDO2 biometric authentication +- **OTP**: One-time password authentication (SMS, Email) +- **Anonymous**: Guest/anonymous session support + +### Security Features + +- **Rate Limiting**: Configurable rate limiting to prevent brute-force attacks +- **CSRF Protection**: Built-in CSRF token validation +- **Session Fingerprinting**: Enhanced session security with device fingerprinting +- **Two-Factor Authentication (2FA)**: TOTP-based 2FA with backup codes +- **Account Linking**: Link multiple authentication methods to a single user account +- **IP-based Rate Limiting**: Prevent attacks from specific IP addresses + +### Session Management + +- Customizable session expiry and renewal +- Secure cookie configuration (HttpOnly, Secure, SameSite) +- Maximum concurrent sessions per user +- Session update intervals + +### Developer Features + +- **Lifecycle Hooks**: `beforeSignIn`, `afterSignIn`, `beforeSignUp`, `afterSignUp`, `beforeSignOut`, `afterSignOut` +- **Database Adapters**: Support for Prisma, Drizzle, Kysely, and custom adapters +- **Email Providers**: Integration with SendGrid, Mailgun, AWS SES, Resend, SMTP +- **Field Mapping**: Map better-auth user fields to your custom user object +- **Plugin System**: Extend better-auth with custom plugins + +## Installation + +```bash +pnpm add @objectstack/plugin-better-auth +``` + +## Usage + +### Basic Example + +```typescript +import type { AuthConfig } from '@objectstack/spec'; + +const authConfig: AuthConfig = { + name: 'main_auth', + label: 'Main Authentication', + driver: 'better-auth', // Optional, defaults to 'better-auth' + strategies: ['email_password'], + baseUrl: 'https://app.example.com', + secret: process.env.AUTH_SECRET!, + + emailPassword: { + enabled: true, + requireEmailVerification: true, + minPasswordLength: 8, + }, + + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, +}; +``` + +### OAuth Example + +```typescript +const oauthConfig: AuthConfig = { + name: 'social_auth', + label: 'Social Login', + strategies: ['oauth'], + baseUrl: 'https://app.example.com', + secret: process.env.AUTH_SECRET!, + + oauth: { + providers: [ + { + provider: 'google', + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + scopes: ['openid', 'profile', 'email'], + }, + ], + }, + + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, +}; +``` + +### Multi-Strategy Example + +```typescript +const multiAuthConfig: AuthConfig = { + name: 'multi_auth', + label: 'Multi-Strategy Auth', + strategies: ['email_password', 'oauth', 'magic_link'], + baseUrl: 'https://app.example.com', + secret: process.env.AUTH_SECRET!, + + emailPassword: { + enabled: true, + minPasswordLength: 10, + }, + + oauth: { + providers: [ + { provider: 'google', clientId: '...', clientSecret: '...' }, + { provider: 'github', clientId: '...', clientSecret: '...' }, + ], + }, + + magicLink: { + enabled: true, + expiryTime: 900, + }, + + session: { + expiresIn: 604800, // 7 days + }, + + rateLimit: { + enabled: true, + maxAttempts: 5, + }, + + csrf: { + enabled: true, + }, + + accountLinking: { + enabled: true, + autoLink: false, + }, +}; +``` + +## Configuration Reference + +### Core Configuration + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `name` | `string` | ✅ | Configuration identifier (snake_case) | +| `label` | `string` | ✅ | Human-readable label | +| `strategies` | `AuthStrategy[]` | ✅ | Enabled authentication strategies | +| `baseUrl` | `string` | ✅ | Application base URL | +| `secret` | `string` | ✅ | Secret key for signing (min 32 chars) | + +### Strategy Configuration + +#### Email/Password + +```typescript +emailPassword: { + enabled: boolean; // Enable email/password auth + requireEmailVerification: boolean; // Require email verification + minPasswordLength: number; // Minimum password length (6-128) + requirePasswordComplexity: boolean; // Require uppercase, lowercase, numbers, symbols + allowPasswordReset: boolean; // Enable password reset + passwordResetExpiry: number; // Reset token expiry in seconds +} +``` + +#### Magic Link + +```typescript +magicLink: { + enabled: boolean; // Enable magic link auth + expiryTime: number; // Link expiry in seconds (default: 900) + sendEmail?: Function; // Custom email sending function +} +``` + +#### Passkey (WebAuthn) + +```typescript +passkey: { + enabled: boolean; // Enable passkey auth + rpName: string; // Relying Party name + rpId?: string; // Relying Party ID (defaults to domain) + allowedOrigins?: string[]; // Allowed origins + userVerification?: 'required' | 'preferred' | 'discouraged'; + attestation?: 'none' | 'indirect' | 'direct' | 'enterprise'; +} +``` + +#### OAuth + +```typescript +oauth: { + providers: [{ + provider: 'google' | 'github' | 'facebook' | ...; // OAuth provider + clientId: string; // OAuth client ID + clientSecret: string; // OAuth client secret + scopes?: string[]; // Requested scopes + redirectUri?: string; // Callback URL + enabled?: boolean; // Enable/disable provider + displayName?: string; // Button label + icon?: string; // Icon URL + }] +} +``` + +### Session Configuration + +```typescript +session: { + expiresIn: number; // Session expiry in seconds (default: 604800) + updateAge: number; // Update interval in seconds (default: 86400) + cookieName: string; // Cookie name + cookieSecure: boolean; // Use secure cookies (HTTPS only) + cookieSameSite: 'strict' | 'lax' | 'none'; // SameSite attribute + cookieDomain?: string; // Cookie domain + cookiePath: string; // Cookie path (default: '/') + cookieHttpOnly: boolean; // HttpOnly attribute +} +``` + +### Security Configuration + +```typescript +rateLimit: { + enabled: boolean; // Enable rate limiting + maxAttempts: number; // Max login attempts (default: 5) + windowMs: number; // Time window in ms (default: 900000) + blockDuration: number; // Block duration in ms + skipSuccessfulRequests: boolean; // Only count failed requests +} + +csrf: { + enabled: boolean; // Enable CSRF protection + tokenLength: number; // CSRF token length (default: 32) + cookieName: string; // CSRF cookie name + headerName: string; // CSRF header name +} + +security: { + allowedOrigins?: string[]; // CORS allowed origins + trustProxy: boolean; // Trust proxy headers + ipRateLimiting: boolean; // Enable IP-based rate limiting + sessionFingerprinting: boolean; // Enable session fingerprinting + maxSessions: number; // Max concurrent sessions (default: 5) +} +``` + +### Two-Factor Authentication + +```typescript +twoFactor: { + enabled: boolean; // Enable 2FA + issuer?: string; // TOTP issuer name + qrCodeSize: number; // QR code size in pixels (default: 200) + backupCodes?: { + enabled: boolean; // Enable backup codes + count: number; // Number of backup codes (default: 10) + } +} +``` + +### Lifecycle Hooks + +```typescript +hooks: { + beforeSignIn?: ({ email }) => Promise; + afterSignIn?: ({ user, session }) => Promise; + beforeSignUp?: ({ email, name? }) => Promise; + afterSignUp?: ({ user }) => Promise; + beforeSignOut?: ({ sessionId }) => Promise; + afterSignOut?: ({ sessionId }) => Promise; +} +``` + +## Supported OAuth Providers + +- Google (`google`) +- GitHub (`github`) +- Facebook (`facebook`) +- Twitter (`twitter`) +- LinkedIn (`linkedin`) +- Microsoft (`microsoft`) +- Apple (`apple`) +- Discord (`discord`) +- GitLab (`gitlab`) +- Custom OAuth2 (`custom`) + +## Database Adapters + +- Prisma (`prisma`) +- Drizzle (`drizzle`) +- Kysely (`kysely`) +- Custom (`custom`) + +## Email Providers + +- SMTP (`smtp`) +- SendGrid (`sendgrid`) +- Mailgun (`mailgun`) +- AWS SES (`ses`) +- Resend (`resend`) +- Custom (`custom`) + +## Examples + +See [examples/auth-better-examples.ts](../examples/auth-better-examples.ts) for comprehensive usage examples including: + +- Basic email/password authentication +- OAuth with Google and GitHub +- Multi-strategy authentication +- Production-ready configuration +- Plugin manifest integration + +## Schema Files + +- **Zod Schema**: `packages/spec/src/system/auth.zod.ts` +- **Tests**: `packages/spec/src/system/auth.test.ts` +- **JSON Schema**: `packages/spec/json-schema/AuthConfig.json` +- **Documentation**: `content/docs/references/system/AuthConfig.mdx` + +## Type Safety + +All schemas are defined using Zod and TypeScript types are inferred automatically: + +```typescript +import type { + AuthConfig, + StandardAuthProvider, + AuthStrategy, + OAuthProvider, + SessionConfig, + // ... and more +} from '@objectstack/spec'; +``` + +## Naming Conventions + +Following ObjectStack conventions: + +- **Configuration Keys** (TypeScript properties): `camelCase` (e.g., `maxAttempts`, `emailPassword`) +- **Machine Names** (Data values): `snake_case` (e.g., `name: 'main_auth'`, `strategy: 'email_password'`) + +## Resources + +- [Better-Auth Documentation](https://better-auth.com) +- [ObjectStack Documentation](https://objectstack.ai) +- [JSON Schema Reference](../packages/spec/json-schema/AuthConfig.json) + +## License + +Apache 2.0 diff --git a/packages/spec/json-schema/AccountLinkingConfig.json b/packages/spec/json-schema/AccountLinkingConfig.json new file mode 100644 index 000000000..f635ac7cc --- /dev/null +++ b/packages/spec/json-schema/AccountLinkingConfig.json @@ -0,0 +1,27 @@ +{ + "$ref": "#/definitions/AccountLinkingConfig", + "definitions": { + "AccountLinkingConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow account linking" + }, + "autoLink": { + "type": "boolean", + "default": false, + "description": "Automatically link accounts with same email" + }, + "requireVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before linking" + } + }, + "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 new file mode 100644 index 000000000..cc76540b5 --- /dev/null +++ b/packages/spec/json-schema/AuthConfig.json @@ -0,0 +1,606 @@ +{ + "$ref": "#/definitions/AuthConfig", + "definitions": { + "AuthConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Configuration name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label" + }, + "driver": { + "type": "string", + "default": "better-auth", + "description": "The underlying authentication implementation driver" + }, + "strategies": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email_password", + "magic_link", + "oauth", + "passkey", + "otp", + "anonymous" + ] + }, + "minItems": 1, + "description": "Enabled authentication strategies" + }, + "baseUrl": { + "type": "string", + "format": "uri", + "description": "Application base URL" + }, + "secret": { + "type": "string", + "minLength": 32, + "description": "Secret key for signing (min 32 chars)" + }, + "emailPassword": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "requireEmailVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before login" + }, + "minPasswordLength": { + "type": "number", + "minimum": 6, + "maximum": 128, + "default": 8, + "description": "Minimum password length" + }, + "requirePasswordComplexity": { + "type": "boolean", + "default": true, + "description": "Require uppercase, lowercase, numbers, symbols" + }, + "allowPasswordReset": { + "type": "boolean", + "default": true, + "description": "Enable password reset functionality" + }, + "passwordResetExpiry": { + "type": "number", + "default": 3600, + "description": "Password reset token expiry in seconds" + } + }, + "additionalProperties": false + }, + "magicLink": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "expiryTime": { + "type": "number", + "default": 900, + "description": "Magic link expiry time in seconds (default 15 min)" + } + }, + "additionalProperties": false + }, + "passkey": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "rpName": { + "type": "string", + "description": "Relying Party name" + }, + "rpId": { + "type": "string", + "description": "Relying Party ID (defaults to domain)" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Allowed origins for WebAuthn" + }, + "userVerification": { + "type": "string", + "enum": [ + "required", + "preferred", + "discouraged" + ], + "default": "preferred" + }, + "attestation": { + "type": "string", + "enum": [ + "none", + "indirect", + "direct", + "enterprise" + ], + "default": "none" + } + }, + "required": [ + "rpName" + ], + "additionalProperties": false + }, + "oauth": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google", + "github", + "facebook", + "twitter", + "linkedin", + "microsoft", + "apple", + "discord", + "gitlab", + "custom" + ], + "description": "OAuth provider type" + }, + "clientId": { + "type": "string", + "description": "OAuth client ID" + }, + "clientSecret": { + "type": "string", + "description": "OAuth client secret (typically from ENV)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Requested OAuth scopes" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "description": "OAuth callback URL" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this provider is enabled" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "provider", + "clientId", + "clientSecret" + ], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": [ + "providers" + ], + "additionalProperties": false + }, + "session": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number", + "default": 604800, + "description": "Session expiry in seconds (default 7 days)" + }, + "updateAge": { + "type": "number", + "default": 86400, + "description": "Session update interval in seconds (default 1 day)" + }, + "cookieName": { + "type": "string", + "default": "session_token", + "description": "Session cookie name" + }, + "cookieSecure": { + "type": "boolean", + "default": true, + "description": "Use secure cookies (HTTPS only)" + }, + "cookieSameSite": { + "type": "string", + "enum": [ + "strict", + "lax", + "none" + ], + "default": "lax", + "description": "SameSite cookie attribute" + }, + "cookieDomain": { + "type": "string", + "description": "Cookie domain" + }, + "cookiePath": { + "type": "string", + "default": "/", + "description": "Cookie path" + }, + "cookieHttpOnly": { + "type": "boolean", + "default": true, + "description": "HttpOnly cookie attribute" + } + }, + "additionalProperties": false, + "default": {} + }, + "rateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "maxAttempts": { + "type": "number", + "default": 5, + "description": "Maximum login attempts" + }, + "windowMs": { + "type": "number", + "default": 900000, + "description": "Time window in milliseconds (default 15 min)" + }, + "blockDuration": { + "type": "number", + "default": 900000, + "description": "Block duration after max attempts in ms" + }, + "skipSuccessfulRequests": { + "type": "boolean", + "default": false, + "description": "Only count failed requests" + } + }, + "additionalProperties": false, + "default": {} + }, + "csrf": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "tokenLength": { + "type": "number", + "default": 32, + "description": "CSRF token length" + }, + "cookieName": { + "type": "string", + "default": "csrf_token", + "description": "CSRF cookie name" + }, + "headerName": { + "type": "string", + "default": "X-CSRF-Token", + "description": "CSRF header name" + } + }, + "additionalProperties": false, + "default": {} + }, + "accountLinking": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow account linking" + }, + "autoLink": { + "type": "boolean", + "default": false, + "description": "Automatically link accounts with same email" + }, + "requireVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before linking" + } + }, + "additionalProperties": false, + "default": {} + }, + "twoFactor": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "description": "TOTP issuer name" + }, + "qrCodeSize": { + "type": "number", + "default": 200, + "description": "QR code size in pixels" + }, + "backupCodes": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "count": { + "type": "number", + "default": 10, + "description": "Number of backup codes to generate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "userFieldMapping": { + "type": "object", + "properties": { + "id": { + "type": "string", + "default": "id", + "description": "User ID field" + }, + "email": { + "type": "string", + "default": "email", + "description": "Email field" + }, + "name": { + "type": "string", + "default": "name", + "description": "Name field" + }, + "image": { + "type": "string", + "default": "image", + "description": "Profile image field" + }, + "emailVerified": { + "type": "string", + "default": "email_verified", + "description": "Email verification status field" + }, + "createdAt": { + "type": "string", + "default": "created_at", + "description": "Created timestamp field" + }, + "updatedAt": { + "type": "string", + "default": "updated_at", + "description": "Updated timestamp field" + } + }, + "additionalProperties": false, + "default": {} + }, + "database": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "prisma", + "drizzle", + "kysely", + "custom" + ], + "description": "Database adapter type" + }, + "connectionString": { + "type": "string", + "description": "Database connection string" + }, + "tablePrefix": { + "type": "string", + "default": "auth_", + "description": "Prefix for auth tables" + }, + "schema": { + "type": "string", + "description": "Database schema name" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "plugins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Plugin name" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "additionalProperties": {}, + "description": "Plugin-specific options" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "default": [] + }, + "hooks": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "description": "Authentication lifecycle hooks" + }, + "security": { + "type": "object", + "properties": { + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed origins" + }, + "trustProxy": { + "type": "boolean", + "default": false, + "description": "Trust proxy headers" + }, + "ipRateLimiting": { + "type": "boolean", + "default": true, + "description": "Enable IP-based rate limiting" + }, + "sessionFingerprinting": { + "type": "boolean", + "default": true, + "description": "Enable session fingerprinting" + }, + "maxSessions": { + "type": "number", + "default": 5, + "description": "Maximum concurrent sessions per user" + } + }, + "additionalProperties": false, + "description": "Advanced security settings" + }, + "email": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "email", + "description": "From email address" + }, + "fromName": { + "type": "string", + "description": "From name" + }, + "provider": { + "type": "string", + "enum": [ + "smtp", + "sendgrid", + "mailgun", + "ses", + "resend", + "custom" + ], + "description": "Email provider" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific configuration" + } + }, + "required": [ + "from", + "provider" + ], + "additionalProperties": false, + "description": "Email configuration" + }, + "ui": { + "type": "object", + "properties": { + "brandName": { + "type": "string", + "description": "Brand name displayed in auth UI" + }, + "logo": { + "type": "string", + "description": "Logo URL" + }, + "primaryColor": { + "type": "string", + "description": "Primary brand color (hex)" + }, + "customCss": { + "type": "string", + "description": "Custom CSS for auth pages" + } + }, + "additionalProperties": false, + "description": "UI customization" + }, + "active": { + "type": "boolean", + "default": true, + "description": "Whether this provider is active" + }, + "allowRegistration": { + "type": "boolean", + "default": true, + "description": "Allow new user registration" + } + }, + "required": [ + "name", + "label", + "strategies", + "baseUrl", + "secret" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/AuthPluginConfig.json b/packages/spec/json-schema/AuthPluginConfig.json new file mode 100644 index 000000000..89e6aa218 --- /dev/null +++ b/packages/spec/json-schema/AuthPluginConfig.json @@ -0,0 +1,28 @@ +{ + "$ref": "#/definitions/AuthPluginConfig", + "definitions": { + "AuthPluginConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Plugin name" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "additionalProperties": {}, + "description": "Plugin-specific options" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/AuthStrategy.json b/packages/spec/json-schema/AuthStrategy.json new file mode 100644 index 000000000..05dd0c08d --- /dev/null +++ b/packages/spec/json-schema/AuthStrategy.json @@ -0,0 +1,17 @@ +{ + "$ref": "#/definitions/AuthStrategy", + "definitions": { + "AuthStrategy": { + "type": "string", + "enum": [ + "email_password", + "magic_link", + "oauth", + "passkey", + "otp", + "anonymous" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/AuthenticationConfig.json b/packages/spec/json-schema/AuthenticationConfig.json new file mode 100644 index 000000000..15e1effbe --- /dev/null +++ b/packages/spec/json-schema/AuthenticationConfig.json @@ -0,0 +1,601 @@ +{ + "$ref": "#/definitions/AuthenticationConfig", + "definitions": { + "AuthenticationConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Configuration name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label" + }, + "strategies": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email_password", + "magic_link", + "oauth", + "passkey", + "otp", + "anonymous" + ] + }, + "minItems": 1, + "description": "Enabled authentication strategies" + }, + "baseUrl": { + "type": "string", + "format": "uri", + "description": "Application base URL" + }, + "secret": { + "type": "string", + "minLength": 32, + "description": "Secret key for signing (min 32 chars)" + }, + "emailPassword": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "requireEmailVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before login" + }, + "minPasswordLength": { + "type": "number", + "minimum": 6, + "maximum": 128, + "default": 8, + "description": "Minimum password length" + }, + "requirePasswordComplexity": { + "type": "boolean", + "default": true, + "description": "Require uppercase, lowercase, numbers, symbols" + }, + "allowPasswordReset": { + "type": "boolean", + "default": true, + "description": "Enable password reset functionality" + }, + "passwordResetExpiry": { + "type": "number", + "default": 3600, + "description": "Password reset token expiry in seconds" + } + }, + "additionalProperties": false + }, + "magicLink": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "expiryTime": { + "type": "number", + "default": 900, + "description": "Magic link expiry time in seconds (default 15 min)" + } + }, + "additionalProperties": false + }, + "passkey": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "rpName": { + "type": "string", + "description": "Relying Party name" + }, + "rpId": { + "type": "string", + "description": "Relying Party ID (defaults to domain)" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Allowed origins for WebAuthn" + }, + "userVerification": { + "type": "string", + "enum": [ + "required", + "preferred", + "discouraged" + ], + "default": "preferred" + }, + "attestation": { + "type": "string", + "enum": [ + "none", + "indirect", + "direct", + "enterprise" + ], + "default": "none" + } + }, + "required": [ + "rpName" + ], + "additionalProperties": false + }, + "oauth": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google", + "github", + "facebook", + "twitter", + "linkedin", + "microsoft", + "apple", + "discord", + "gitlab", + "custom" + ], + "description": "OAuth provider type" + }, + "clientId": { + "type": "string", + "description": "OAuth client ID" + }, + "clientSecret": { + "type": "string", + "description": "OAuth client secret (typically from ENV)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Requested OAuth scopes" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "description": "OAuth callback URL" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this provider is enabled" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "provider", + "clientId", + "clientSecret" + ], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": [ + "providers" + ], + "additionalProperties": false + }, + "session": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number", + "default": 604800, + "description": "Session expiry in seconds (default 7 days)" + }, + "updateAge": { + "type": "number", + "default": 86400, + "description": "Session update interval in seconds (default 1 day)" + }, + "cookieName": { + "type": "string", + "default": "session_token", + "description": "Session cookie name" + }, + "cookieSecure": { + "type": "boolean", + "default": true, + "description": "Use secure cookies (HTTPS only)" + }, + "cookieSameSite": { + "type": "string", + "enum": [ + "strict", + "lax", + "none" + ], + "default": "lax", + "description": "SameSite cookie attribute" + }, + "cookieDomain": { + "type": "string", + "description": "Cookie domain" + }, + "cookiePath": { + "type": "string", + "default": "/", + "description": "Cookie path" + }, + "cookieHttpOnly": { + "type": "boolean", + "default": true, + "description": "HttpOnly cookie attribute" + } + }, + "additionalProperties": false, + "default": {} + }, + "rateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "maxAttempts": { + "type": "number", + "default": 5, + "description": "Maximum login attempts" + }, + "windowMs": { + "type": "number", + "default": 900000, + "description": "Time window in milliseconds (default 15 min)" + }, + "blockDuration": { + "type": "number", + "default": 900000, + "description": "Block duration after max attempts in ms" + }, + "skipSuccessfulRequests": { + "type": "boolean", + "default": false, + "description": "Only count failed requests" + } + }, + "additionalProperties": false, + "default": {} + }, + "csrf": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "tokenLength": { + "type": "number", + "default": 32, + "description": "CSRF token length" + }, + "cookieName": { + "type": "string", + "default": "csrf_token", + "description": "CSRF cookie name" + }, + "headerName": { + "type": "string", + "default": "X-CSRF-Token", + "description": "CSRF header name" + } + }, + "additionalProperties": false, + "default": {} + }, + "accountLinking": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow account linking" + }, + "autoLink": { + "type": "boolean", + "default": false, + "description": "Automatically link accounts with same email" + }, + "requireVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before linking" + } + }, + "additionalProperties": false, + "default": {} + }, + "twoFactor": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "description": "TOTP issuer name" + }, + "qrCodeSize": { + "type": "number", + "default": 200, + "description": "QR code size in pixels" + }, + "backupCodes": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "count": { + "type": "number", + "default": 10, + "description": "Number of backup codes to generate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "userFieldMapping": { + "type": "object", + "properties": { + "id": { + "type": "string", + "default": "id", + "description": "User ID field" + }, + "email": { + "type": "string", + "default": "email", + "description": "Email field" + }, + "name": { + "type": "string", + "default": "name", + "description": "Name field" + }, + "image": { + "type": "string", + "default": "image", + "description": "Profile image field" + }, + "emailVerified": { + "type": "string", + "default": "email_verified", + "description": "Email verification status field" + }, + "createdAt": { + "type": "string", + "default": "created_at", + "description": "Created timestamp field" + }, + "updatedAt": { + "type": "string", + "default": "updated_at", + "description": "Updated timestamp field" + } + }, + "additionalProperties": false, + "default": {} + }, + "database": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "prisma", + "drizzle", + "kysely", + "custom" + ], + "description": "Database adapter type" + }, + "connectionString": { + "type": "string", + "description": "Database connection string" + }, + "tablePrefix": { + "type": "string", + "default": "auth_", + "description": "Prefix for auth tables" + }, + "schema": { + "type": "string", + "description": "Database schema name" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "plugins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Plugin name" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "additionalProperties": {}, + "description": "Plugin-specific options" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "default": [] + }, + "hooks": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "description": "Authentication lifecycle hooks" + }, + "security": { + "type": "object", + "properties": { + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed origins" + }, + "trustProxy": { + "type": "boolean", + "default": false, + "description": "Trust proxy headers" + }, + "ipRateLimiting": { + "type": "boolean", + "default": true, + "description": "Enable IP-based rate limiting" + }, + "sessionFingerprinting": { + "type": "boolean", + "default": true, + "description": "Enable session fingerprinting" + }, + "maxSessions": { + "type": "number", + "default": 5, + "description": "Maximum concurrent sessions per user" + } + }, + "additionalProperties": false, + "description": "Advanced security settings" + }, + "email": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "email", + "description": "From email address" + }, + "fromName": { + "type": "string", + "description": "From name" + }, + "provider": { + "type": "string", + "enum": [ + "smtp", + "sendgrid", + "mailgun", + "ses", + "resend", + "custom" + ], + "description": "Email provider" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific configuration" + } + }, + "required": [ + "from", + "provider" + ], + "additionalProperties": false, + "description": "Email configuration" + }, + "ui": { + "type": "object", + "properties": { + "brandName": { + "type": "string", + "description": "Brand name displayed in auth UI" + }, + "logo": { + "type": "string", + "description": "Logo URL" + }, + "primaryColor": { + "type": "string", + "description": "Primary brand color (hex)" + }, + "customCss": { + "type": "string", + "description": "Custom CSS for auth pages" + } + }, + "additionalProperties": false, + "description": "UI customization" + }, + "active": { + "type": "boolean", + "default": true, + "description": "Whether this provider is active" + }, + "allowRegistration": { + "type": "boolean", + "default": true, + "description": "Allow new user registration" + } + }, + "required": [ + "name", + "label", + "strategies", + "baseUrl", + "secret" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/AuthenticationProvider.json b/packages/spec/json-schema/AuthenticationProvider.json new file mode 100644 index 000000000..394907760 --- /dev/null +++ b/packages/spec/json-schema/AuthenticationProvider.json @@ -0,0 +1,617 @@ +{ + "$ref": "#/definitions/AuthenticationProvider", + "definitions": { + "AuthenticationProvider": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authentication", + "description": "Provider type identifier" + }, + "config": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Configuration name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label" + }, + "strategies": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email_password", + "magic_link", + "oauth", + "passkey", + "otp", + "anonymous" + ] + }, + "minItems": 1, + "description": "Enabled authentication strategies" + }, + "baseUrl": { + "type": "string", + "format": "uri", + "description": "Application base URL" + }, + "secret": { + "type": "string", + "minLength": 32, + "description": "Secret key for signing (min 32 chars)" + }, + "emailPassword": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "requireEmailVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before login" + }, + "minPasswordLength": { + "type": "number", + "minimum": 6, + "maximum": 128, + "default": 8, + "description": "Minimum password length" + }, + "requirePasswordComplexity": { + "type": "boolean", + "default": true, + "description": "Require uppercase, lowercase, numbers, symbols" + }, + "allowPasswordReset": { + "type": "boolean", + "default": true, + "description": "Enable password reset functionality" + }, + "passwordResetExpiry": { + "type": "number", + "default": 3600, + "description": "Password reset token expiry in seconds" + } + }, + "additionalProperties": false + }, + "magicLink": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "expiryTime": { + "type": "number", + "default": 900, + "description": "Magic link expiry time in seconds (default 15 min)" + } + }, + "additionalProperties": false + }, + "passkey": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "rpName": { + "type": "string", + "description": "Relying Party name" + }, + "rpId": { + "type": "string", + "description": "Relying Party ID (defaults to domain)" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Allowed origins for WebAuthn" + }, + "userVerification": { + "type": "string", + "enum": [ + "required", + "preferred", + "discouraged" + ], + "default": "preferred" + }, + "attestation": { + "type": "string", + "enum": [ + "none", + "indirect", + "direct", + "enterprise" + ], + "default": "none" + } + }, + "required": [ + "rpName" + ], + "additionalProperties": false + }, + "oauth": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google", + "github", + "facebook", + "twitter", + "linkedin", + "microsoft", + "apple", + "discord", + "gitlab", + "custom" + ], + "description": "OAuth provider type" + }, + "clientId": { + "type": "string", + "description": "OAuth client ID" + }, + "clientSecret": { + "type": "string", + "description": "OAuth client secret (typically from ENV)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Requested OAuth scopes" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "description": "OAuth callback URL" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this provider is enabled" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "provider", + "clientId", + "clientSecret" + ], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": [ + "providers" + ], + "additionalProperties": false + }, + "session": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number", + "default": 604800, + "description": "Session expiry in seconds (default 7 days)" + }, + "updateAge": { + "type": "number", + "default": 86400, + "description": "Session update interval in seconds (default 1 day)" + }, + "cookieName": { + "type": "string", + "default": "session_token", + "description": "Session cookie name" + }, + "cookieSecure": { + "type": "boolean", + "default": true, + "description": "Use secure cookies (HTTPS only)" + }, + "cookieSameSite": { + "type": "string", + "enum": [ + "strict", + "lax", + "none" + ], + "default": "lax", + "description": "SameSite cookie attribute" + }, + "cookieDomain": { + "type": "string", + "description": "Cookie domain" + }, + "cookiePath": { + "type": "string", + "default": "/", + "description": "Cookie path" + }, + "cookieHttpOnly": { + "type": "boolean", + "default": true, + "description": "HttpOnly cookie attribute" + } + }, + "additionalProperties": false, + "default": {} + }, + "rateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "maxAttempts": { + "type": "number", + "default": 5, + "description": "Maximum login attempts" + }, + "windowMs": { + "type": "number", + "default": 900000, + "description": "Time window in milliseconds (default 15 min)" + }, + "blockDuration": { + "type": "number", + "default": 900000, + "description": "Block duration after max attempts in ms" + }, + "skipSuccessfulRequests": { + "type": "boolean", + "default": false, + "description": "Only count failed requests" + } + }, + "additionalProperties": false, + "default": {} + }, + "csrf": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "tokenLength": { + "type": "number", + "default": 32, + "description": "CSRF token length" + }, + "cookieName": { + "type": "string", + "default": "csrf_token", + "description": "CSRF cookie name" + }, + "headerName": { + "type": "string", + "default": "X-CSRF-Token", + "description": "CSRF header name" + } + }, + "additionalProperties": false, + "default": {} + }, + "accountLinking": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow account linking" + }, + "autoLink": { + "type": "boolean", + "default": false, + "description": "Automatically link accounts with same email" + }, + "requireVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before linking" + } + }, + "additionalProperties": false, + "default": {} + }, + "twoFactor": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "description": "TOTP issuer name" + }, + "qrCodeSize": { + "type": "number", + "default": 200, + "description": "QR code size in pixels" + }, + "backupCodes": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "count": { + "type": "number", + "default": 10, + "description": "Number of backup codes to generate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "userFieldMapping": { + "type": "object", + "properties": { + "id": { + "type": "string", + "default": "id", + "description": "User ID field" + }, + "email": { + "type": "string", + "default": "email", + "description": "Email field" + }, + "name": { + "type": "string", + "default": "name", + "description": "Name field" + }, + "image": { + "type": "string", + "default": "image", + "description": "Profile image field" + }, + "emailVerified": { + "type": "string", + "default": "email_verified", + "description": "Email verification status field" + }, + "createdAt": { + "type": "string", + "default": "created_at", + "description": "Created timestamp field" + }, + "updatedAt": { + "type": "string", + "default": "updated_at", + "description": "Updated timestamp field" + } + }, + "additionalProperties": false, + "default": {} + }, + "database": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "prisma", + "drizzle", + "kysely", + "custom" + ], + "description": "Database adapter type" + }, + "connectionString": { + "type": "string", + "description": "Database connection string" + }, + "tablePrefix": { + "type": "string", + "default": "auth_", + "description": "Prefix for auth tables" + }, + "schema": { + "type": "string", + "description": "Database schema name" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "plugins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Plugin name" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "additionalProperties": {}, + "description": "Plugin-specific options" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "default": [] + }, + "hooks": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "description": "Authentication lifecycle hooks" + }, + "security": { + "type": "object", + "properties": { + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed origins" + }, + "trustProxy": { + "type": "boolean", + "default": false, + "description": "Trust proxy headers" + }, + "ipRateLimiting": { + "type": "boolean", + "default": true, + "description": "Enable IP-based rate limiting" + }, + "sessionFingerprinting": { + "type": "boolean", + "default": true, + "description": "Enable session fingerprinting" + }, + "maxSessions": { + "type": "number", + "default": 5, + "description": "Maximum concurrent sessions per user" + } + }, + "additionalProperties": false, + "description": "Advanced security settings" + }, + "email": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "email", + "description": "From email address" + }, + "fromName": { + "type": "string", + "description": "From name" + }, + "provider": { + "type": "string", + "enum": [ + "smtp", + "sendgrid", + "mailgun", + "ses", + "resend", + "custom" + ], + "description": "Email provider" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific configuration" + } + }, + "required": [ + "from", + "provider" + ], + "additionalProperties": false, + "description": "Email configuration" + }, + "ui": { + "type": "object", + "properties": { + "brandName": { + "type": "string", + "description": "Brand name displayed in auth UI" + }, + "logo": { + "type": "string", + "description": "Logo URL" + }, + "primaryColor": { + "type": "string", + "description": "Primary brand color (hex)" + }, + "customCss": { + "type": "string", + "description": "Custom CSS for auth pages" + } + }, + "additionalProperties": false, + "description": "UI customization" + }, + "active": { + "type": "boolean", + "default": true, + "description": "Whether this provider is active" + }, + "allowRegistration": { + "type": "boolean", + "default": true, + "description": "Allow new user registration" + } + }, + "required": [ + "name", + "label", + "strategies", + "baseUrl", + "secret" + ], + "additionalProperties": false, + "description": "Authentication configuration" + } + }, + "required": [ + "type", + "config" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/CSRFConfig.json b/packages/spec/json-schema/CSRFConfig.json new file mode 100644 index 000000000..7c49ed70e --- /dev/null +++ b/packages/spec/json-schema/CSRFConfig.json @@ -0,0 +1,31 @@ +{ + "$ref": "#/definitions/CSRFConfig", + "definitions": { + "CSRFConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "tokenLength": { + "type": "number", + "default": 32, + "description": "CSRF token length" + }, + "cookieName": { + "type": "string", + "default": "csrf_token", + "description": "CSRF cookie name" + }, + "headerName": { + "type": "string", + "default": "X-CSRF-Token", + "description": "CSRF header name" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/DatabaseAdapter.json b/packages/spec/json-schema/DatabaseAdapter.json new file mode 100644 index 000000000..4695c1a23 --- /dev/null +++ b/packages/spec/json-schema/DatabaseAdapter.json @@ -0,0 +1,38 @@ +{ + "$ref": "#/definitions/DatabaseAdapter", + "definitions": { + "DatabaseAdapter": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "prisma", + "drizzle", + "kysely", + "custom" + ], + "description": "Database adapter type" + }, + "connectionString": { + "type": "string", + "description": "Database connection string" + }, + "tablePrefix": { + "type": "string", + "default": "auth_", + "description": "Prefix for auth tables" + }, + "schema": { + "type": "string", + "description": "Database schema name" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/EmailPasswordConfig.json b/packages/spec/json-schema/EmailPasswordConfig.json new file mode 100644 index 000000000..9bb0c155b --- /dev/null +++ b/packages/spec/json-schema/EmailPasswordConfig.json @@ -0,0 +1,43 @@ +{ + "$ref": "#/definitions/EmailPasswordConfig", + "definitions": { + "EmailPasswordConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "requireEmailVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before login" + }, + "minPasswordLength": { + "type": "number", + "minimum": 6, + "maximum": 128, + "default": 8, + "description": "Minimum password length" + }, + "requirePasswordComplexity": { + "type": "boolean", + "default": true, + "description": "Require uppercase, lowercase, numbers, symbols" + }, + "allowPasswordReset": { + "type": "boolean", + "default": true, + "description": "Enable password reset functionality" + }, + "passwordResetExpiry": { + "type": "number", + "default": 3600, + "description": "Password reset token expiry in seconds" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/MagicLinkConfig.json b/packages/spec/json-schema/MagicLinkConfig.json new file mode 100644 index 000000000..937680a51 --- /dev/null +++ b/packages/spec/json-schema/MagicLinkConfig.json @@ -0,0 +1,21 @@ +{ + "$ref": "#/definitions/MagicLinkConfig", + "definitions": { + "MagicLinkConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "expiryTime": { + "type": "number", + "default": 900, + "description": "Magic link expiry time in seconds (default 15 min)" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/OAuthProvider.json b/packages/spec/json-schema/OAuthProvider.json new file mode 100644 index 000000000..738d846ad --- /dev/null +++ b/packages/spec/json-schema/OAuthProvider.json @@ -0,0 +1,66 @@ +{ + "$ref": "#/definitions/OAuthProvider", + "definitions": { + "OAuthProvider": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google", + "github", + "facebook", + "twitter", + "linkedin", + "microsoft", + "apple", + "discord", + "gitlab", + "custom" + ], + "description": "OAuth provider type" + }, + "clientId": { + "type": "string", + "description": "OAuth client ID" + }, + "clientSecret": { + "type": "string", + "description": "OAuth client secret (typically from ENV)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Requested OAuth scopes" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "description": "OAuth callback URL" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this provider is enabled" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "provider", + "clientId", + "clientSecret" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/PasskeyConfig.json b/packages/spec/json-schema/PasskeyConfig.json new file mode 100644 index 000000000..335a54344 --- /dev/null +++ b/packages/spec/json-schema/PasskeyConfig.json @@ -0,0 +1,54 @@ +{ + "$ref": "#/definitions/PasskeyConfig", + "definitions": { + "PasskeyConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "rpName": { + "type": "string", + "description": "Relying Party name" + }, + "rpId": { + "type": "string", + "description": "Relying Party ID (defaults to domain)" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Allowed origins for WebAuthn" + }, + "userVerification": { + "type": "string", + "enum": [ + "required", + "preferred", + "discouraged" + ], + "default": "preferred" + }, + "attestation": { + "type": "string", + "enum": [ + "none", + "indirect", + "direct", + "enterprise" + ], + "default": "none" + } + }, + "required": [ + "rpName" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/RateLimitConfig.json b/packages/spec/json-schema/RateLimitConfig.json new file mode 100644 index 000000000..d8620aadb --- /dev/null +++ b/packages/spec/json-schema/RateLimitConfig.json @@ -0,0 +1,36 @@ +{ + "$ref": "#/definitions/RateLimitConfig", + "definitions": { + "RateLimitConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "maxAttempts": { + "type": "number", + "default": 5, + "description": "Maximum login attempts" + }, + "windowMs": { + "type": "number", + "default": 900000, + "description": "Time window in milliseconds (default 15 min)" + }, + "blockDuration": { + "type": "number", + "default": 900000, + "description": "Block duration after max attempts in ms" + }, + "skipSuccessfulRequests": { + "type": "boolean", + "default": false, + "description": "Only count failed requests" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/SessionConfig.json b/packages/spec/json-schema/SessionConfig.json new file mode 100644 index 000000000..a1d9b75bf --- /dev/null +++ b/packages/spec/json-schema/SessionConfig.json @@ -0,0 +1,56 @@ +{ + "$ref": "#/definitions/SessionConfig", + "definitions": { + "SessionConfig": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number", + "default": 604800, + "description": "Session expiry in seconds (default 7 days)" + }, + "updateAge": { + "type": "number", + "default": 86400, + "description": "Session update interval in seconds (default 1 day)" + }, + "cookieName": { + "type": "string", + "default": "session_token", + "description": "Session cookie name" + }, + "cookieSecure": { + "type": "boolean", + "default": true, + "description": "Use secure cookies (HTTPS only)" + }, + "cookieSameSite": { + "type": "string", + "enum": [ + "strict", + "lax", + "none" + ], + "default": "lax", + "description": "SameSite cookie attribute" + }, + "cookieDomain": { + "type": "string", + "description": "Cookie domain" + }, + "cookiePath": { + "type": "string", + "default": "/", + "description": "Cookie path" + }, + "cookieHttpOnly": { + "type": "boolean", + "default": true, + "description": "HttpOnly cookie attribute" + } + }, + "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 new file mode 100644 index 000000000..67753239b --- /dev/null +++ b/packages/spec/json-schema/StandardAuthProvider.json @@ -0,0 +1,622 @@ +{ + "$ref": "#/definitions/StandardAuthProvider", + "definitions": { + "StandardAuthProvider": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "standard_auth", + "description": "Provider type identifier" + }, + "config": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Configuration name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label" + }, + "driver": { + "type": "string", + "default": "better-auth", + "description": "The underlying authentication implementation driver" + }, + "strategies": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email_password", + "magic_link", + "oauth", + "passkey", + "otp", + "anonymous" + ] + }, + "minItems": 1, + "description": "Enabled authentication strategies" + }, + "baseUrl": { + "type": "string", + "format": "uri", + "description": "Application base URL" + }, + "secret": { + "type": "string", + "minLength": 32, + "description": "Secret key for signing (min 32 chars)" + }, + "emailPassword": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "requireEmailVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before login" + }, + "minPasswordLength": { + "type": "number", + "minimum": 6, + "maximum": 128, + "default": 8, + "description": "Minimum password length" + }, + "requirePasswordComplexity": { + "type": "boolean", + "default": true, + "description": "Require uppercase, lowercase, numbers, symbols" + }, + "allowPasswordReset": { + "type": "boolean", + "default": true, + "description": "Enable password reset functionality" + }, + "passwordResetExpiry": { + "type": "number", + "default": 3600, + "description": "Password reset token expiry in seconds" + } + }, + "additionalProperties": false + }, + "magicLink": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "expiryTime": { + "type": "number", + "default": 900, + "description": "Magic link expiry time in seconds (default 15 min)" + } + }, + "additionalProperties": false + }, + "passkey": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "rpName": { + "type": "string", + "description": "Relying Party name" + }, + "rpId": { + "type": "string", + "description": "Relying Party ID (defaults to domain)" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Allowed origins for WebAuthn" + }, + "userVerification": { + "type": "string", + "enum": [ + "required", + "preferred", + "discouraged" + ], + "default": "preferred" + }, + "attestation": { + "type": "string", + "enum": [ + "none", + "indirect", + "direct", + "enterprise" + ], + "default": "none" + } + }, + "required": [ + "rpName" + ], + "additionalProperties": false + }, + "oauth": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google", + "github", + "facebook", + "twitter", + "linkedin", + "microsoft", + "apple", + "discord", + "gitlab", + "custom" + ], + "description": "OAuth provider type" + }, + "clientId": { + "type": "string", + "description": "OAuth client ID" + }, + "clientSecret": { + "type": "string", + "description": "OAuth client secret (typically from ENV)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Requested OAuth scopes" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "description": "OAuth callback URL" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this provider is enabled" + }, + "displayName": { + "type": "string", + "description": "Display name for the provider button" + }, + "icon": { + "type": "string", + "description": "Icon URL or identifier" + } + }, + "required": [ + "provider", + "clientId", + "clientSecret" + ], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": [ + "providers" + ], + "additionalProperties": false + }, + "session": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number", + "default": 604800, + "description": "Session expiry in seconds (default 7 days)" + }, + "updateAge": { + "type": "number", + "default": 86400, + "description": "Session update interval in seconds (default 1 day)" + }, + "cookieName": { + "type": "string", + "default": "session_token", + "description": "Session cookie name" + }, + "cookieSecure": { + "type": "boolean", + "default": true, + "description": "Use secure cookies (HTTPS only)" + }, + "cookieSameSite": { + "type": "string", + "enum": [ + "strict", + "lax", + "none" + ], + "default": "lax", + "description": "SameSite cookie attribute" + }, + "cookieDomain": { + "type": "string", + "description": "Cookie domain" + }, + "cookiePath": { + "type": "string", + "default": "/", + "description": "Cookie path" + }, + "cookieHttpOnly": { + "type": "boolean", + "default": true, + "description": "HttpOnly cookie attribute" + } + }, + "additionalProperties": false, + "default": {} + }, + "rateLimit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "maxAttempts": { + "type": "number", + "default": 5, + "description": "Maximum login attempts" + }, + "windowMs": { + "type": "number", + "default": 900000, + "description": "Time window in milliseconds (default 15 min)" + }, + "blockDuration": { + "type": "number", + "default": 900000, + "description": "Block duration after max attempts in ms" + }, + "skipSuccessfulRequests": { + "type": "boolean", + "default": false, + "description": "Only count failed requests" + } + }, + "additionalProperties": false, + "default": {} + }, + "csrf": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "tokenLength": { + "type": "number", + "default": 32, + "description": "CSRF token length" + }, + "cookieName": { + "type": "string", + "default": "csrf_token", + "description": "CSRF cookie name" + }, + "headerName": { + "type": "string", + "default": "X-CSRF-Token", + "description": "CSRF header name" + } + }, + "additionalProperties": false, + "default": {} + }, + "accountLinking": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Allow account linking" + }, + "autoLink": { + "type": "boolean", + "default": false, + "description": "Automatically link accounts with same email" + }, + "requireVerification": { + "type": "boolean", + "default": true, + "description": "Require email verification before linking" + } + }, + "additionalProperties": false, + "default": {} + }, + "twoFactor": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "description": "TOTP issuer name" + }, + "qrCodeSize": { + "type": "number", + "default": 200, + "description": "QR code size in pixels" + }, + "backupCodes": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "count": { + "type": "number", + "default": 10, + "description": "Number of backup codes to generate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "userFieldMapping": { + "type": "object", + "properties": { + "id": { + "type": "string", + "default": "id", + "description": "User ID field" + }, + "email": { + "type": "string", + "default": "email", + "description": "Email field" + }, + "name": { + "type": "string", + "default": "name", + "description": "Name field" + }, + "image": { + "type": "string", + "default": "image", + "description": "Profile image field" + }, + "emailVerified": { + "type": "string", + "default": "email_verified", + "description": "Email verification status field" + }, + "createdAt": { + "type": "string", + "default": "created_at", + "description": "Created timestamp field" + }, + "updatedAt": { + "type": "string", + "default": "updated_at", + "description": "Updated timestamp field" + } + }, + "additionalProperties": false, + "default": {} + }, + "database": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "prisma", + "drizzle", + "kysely", + "custom" + ], + "description": "Database adapter type" + }, + "connectionString": { + "type": "string", + "description": "Database connection string" + }, + "tablePrefix": { + "type": "string", + "default": "auth_", + "description": "Prefix for auth tables" + }, + "schema": { + "type": "string", + "description": "Database schema name" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "plugins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Plugin name" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "additionalProperties": {}, + "description": "Plugin-specific options" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "default": [] + }, + "hooks": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "description": "Authentication lifecycle hooks" + }, + "security": { + "type": "object", + "properties": { + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CORS allowed origins" + }, + "trustProxy": { + "type": "boolean", + "default": false, + "description": "Trust proxy headers" + }, + "ipRateLimiting": { + "type": "boolean", + "default": true, + "description": "Enable IP-based rate limiting" + }, + "sessionFingerprinting": { + "type": "boolean", + "default": true, + "description": "Enable session fingerprinting" + }, + "maxSessions": { + "type": "number", + "default": 5, + "description": "Maximum concurrent sessions per user" + } + }, + "additionalProperties": false, + "description": "Advanced security settings" + }, + "email": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "email", + "description": "From email address" + }, + "fromName": { + "type": "string", + "description": "From name" + }, + "provider": { + "type": "string", + "enum": [ + "smtp", + "sendgrid", + "mailgun", + "ses", + "resend", + "custom" + ], + "description": "Email provider" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific configuration" + } + }, + "required": [ + "from", + "provider" + ], + "additionalProperties": false, + "description": "Email configuration" + }, + "ui": { + "type": "object", + "properties": { + "brandName": { + "type": "string", + "description": "Brand name displayed in auth UI" + }, + "logo": { + "type": "string", + "description": "Logo URL" + }, + "primaryColor": { + "type": "string", + "description": "Primary brand color (hex)" + }, + "customCss": { + "type": "string", + "description": "Custom CSS for auth pages" + } + }, + "additionalProperties": false, + "description": "UI customization" + }, + "active": { + "type": "boolean", + "default": true, + "description": "Whether this provider is active" + }, + "allowRegistration": { + "type": "boolean", + "default": true, + "description": "Allow new user registration" + } + }, + "required": [ + "name", + "label", + "strategies", + "baseUrl", + "secret" + ], + "additionalProperties": false, + "description": "Standard authentication configuration" + } + }, + "required": [ + "type", + "config" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/TwoFactorConfig.json b/packages/spec/json-schema/TwoFactorConfig.json new file mode 100644 index 000000000..488c80a3b --- /dev/null +++ b/packages/spec/json-schema/TwoFactorConfig.json @@ -0,0 +1,40 @@ +{ + "$ref": "#/definitions/TwoFactorConfig", + "definitions": { + "TwoFactorConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "issuer": { + "type": "string", + "description": "TOTP issuer name" + }, + "qrCodeSize": { + "type": "number", + "default": 200, + "description": "QR code size in pixels" + }, + "backupCodes": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "count": { + "type": "number", + "default": 10, + "description": "Number of backup codes to generate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/UserFieldMapping.json b/packages/spec/json-schema/UserFieldMapping.json new file mode 100644 index 000000000..a07a8458c --- /dev/null +++ b/packages/spec/json-schema/UserFieldMapping.json @@ -0,0 +1,47 @@ +{ + "$ref": "#/definitions/UserFieldMapping", + "definitions": { + "UserFieldMapping": { + "type": "object", + "properties": { + "id": { + "type": "string", + "default": "id", + "description": "User ID field" + }, + "email": { + "type": "string", + "default": "email", + "description": "Email field" + }, + "name": { + "type": "string", + "default": "name", + "description": "Name field" + }, + "image": { + "type": "string", + "default": "image", + "description": "Profile image field" + }, + "emailVerified": { + "type": "string", + "default": "email_verified", + "description": "Email verification status field" + }, + "createdAt": { + "type": "string", + "default": "created_at", + "description": "Created timestamp field" + }, + "updatedAt": { + "type": "string", + "default": "updated_at", + "description": "Updated timestamp field" + } + }, + "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 80f8750d1..c82e5fd33 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -41,6 +41,7 @@ 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/policy.zod'; export * from './system/role.zod'; export * from './system/territory.zod'; diff --git a/packages/spec/src/system/auth.test.ts b/packages/spec/src/system/auth.test.ts new file mode 100644 index 000000000..e63d1c4f7 --- /dev/null +++ b/packages/spec/src/system/auth.test.ts @@ -0,0 +1,752 @@ +import { describe, it, expect } from 'vitest'; +import { + AuthStrategy, + OAuthProviderSchema, + EmailPasswordConfigSchema, + MagicLinkConfigSchema, + PasskeyConfigSchema, + SessionConfigSchema, + RateLimitConfigSchema, + CSRFConfigSchema, + AccountLinkingConfigSchema, + TwoFactorConfigSchema, + UserFieldMappingSchema, + DatabaseAdapterSchema, + AuthPluginConfigSchema, + AuthConfigSchema, + StandardAuthProviderSchema, + type AuthConfig, + type StandardAuthProvider, + type OAuthProvider, +} from "./auth.zod"; + +describe('AuthStrategy', () => { + it('should accept valid authentication strategies', () => { + const strategies = [ + 'email_password', + 'magic_link', + 'oauth', + 'passkey', + 'otp', + 'anonymous', + ]; + + strategies.forEach((strategy) => { + expect(() => AuthStrategy.parse(strategy)).not.toThrow(); + }); + }); + + it('should reject invalid strategies', () => { + expect(() => AuthStrategy.parse('invalid')).toThrow(); + }); +}); + +describe('OAuthProviderSchema', () => { + it('should accept valid OAuth provider configuration', () => { + const provider: OAuthProvider = { + provider: 'google', + clientId: 'test-client-id', + clientSecret: 'test-client-secret', + scopes: ['openid', 'profile', 'email'], + enabled: true, + }; + + expect(() => OAuthProviderSchema.parse(provider)).not.toThrow(); + }); + + it('should accept minimal OAuth provider configuration', () => { + const provider = { + provider: 'github', + clientId: 'test-client-id', + clientSecret: 'test-client-secret', + }; + + const result = OAuthProviderSchema.parse(provider); + expect(result.enabled).toBe(true); // default value + }); + + it('should accept all supported OAuth providers', () => { + const providers = [ + 'google', + 'github', + 'facebook', + 'twitter', + 'linkedin', + 'microsoft', + 'apple', + 'discord', + 'gitlab', + 'custom', + ]; + + providers.forEach((provider) => { + const config = { + provider, + clientId: 'test-id', + clientSecret: 'test-secret', + }; + expect(() => OAuthProviderSchema.parse(config)).not.toThrow(); + }); + }); + + it('should validate redirect URI is a valid URL', () => { + const provider = { + provider: 'google', + clientId: 'test-id', + clientSecret: 'test-secret', + redirectUri: 'not-a-url', + }; + + expect(() => OAuthProviderSchema.parse(provider)).toThrow(); + }); +}); + +describe('EmailPasswordConfigSchema', () => { + it('should accept valid email password configuration', () => { + const config = { + enabled: true, + requireEmailVerification: true, + minPasswordLength: 12, + requirePasswordComplexity: true, + allowPasswordReset: true, + passwordResetExpiry: 3600, + }; + + expect(() => EmailPasswordConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = EmailPasswordConfigSchema.parse(config); + + expect(result.enabled).toBe(true); + expect(result.minPasswordLength).toBe(8); + expect(result.requireEmailVerification).toBe(true); + }); + + it('should enforce password length constraints', () => { + const tooShort = { minPasswordLength: 5 }; + expect(() => EmailPasswordConfigSchema.parse(tooShort)).toThrow(); + + const tooLong = { minPasswordLength: 200 }; + expect(() => EmailPasswordConfigSchema.parse(tooLong)).toThrow(); + + const justRight = { minPasswordLength: 10 }; + expect(() => EmailPasswordConfigSchema.parse(justRight)).not.toThrow(); + }); +}); + +describe('MagicLinkConfigSchema', () => { + it('should accept valid magic link configuration', () => { + const config = { + enabled: true, + expiryTime: 1800, // 30 minutes + }; + + expect(() => MagicLinkConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default expiry time', () => { + const config = { enabled: true }; + const result = MagicLinkConfigSchema.parse(config); + + expect(result.expiryTime).toBe(900); // 15 minutes default + }); +}); + +describe('PasskeyConfigSchema', () => { + it('should accept valid passkey configuration', () => { + const config = { + enabled: true, + rpName: 'ObjectStack', + rpId: 'objectstack.com', + userVerification: 'required' as const, + attestation: 'direct' as const, + }; + + expect(() => PasskeyConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = { + rpName: 'Test App', + }; + const result = PasskeyConfigSchema.parse(config); + + expect(result.enabled).toBe(false); // disabled by default + expect(result.userVerification).toBe('preferred'); + expect(result.attestation).toBe('none'); + }); + + it('should validate allowed origins are URLs', () => { + const config = { + rpName: 'Test', + allowedOrigins: ['https://example.com', 'https://app.example.com'], + }; + + expect(() => PasskeyConfigSchema.parse(config)).not.toThrow(); + }); +}); + +describe('SessionConfigSchema', () => { + it('should accept valid session configuration', () => { + const config = { + expiresIn: 604800, // 7 days + updateAge: 86400, // 1 day + cookieName: 'my-session', + cookieSecure: true, + cookieSameSite: 'strict' as const, + cookieHttpOnly: true, + }; + + expect(() => SessionConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = SessionConfigSchema.parse(config); + + expect(result.expiresIn).toBe(86400 * 7); // 7 days + expect(result.cookieName).toBe('session_token'); + expect(result.cookieSecure).toBe(true); + expect(result.cookieSameSite).toBe('lax'); + }); + + it('should accept all SameSite options', () => { + const options = ['strict', 'lax', 'none'] as const; + + options.forEach((sameSite) => { + const config = { cookieSameSite: sameSite }; + expect(() => SessionConfigSchema.parse(config)).not.toThrow(); + }); + }); +}); + +describe('RateLimitConfigSchema', () => { + it('should accept valid rate limit configuration', () => { + const config = { + enabled: true, + maxAttempts: 10, + windowMs: 1800000, // 30 minutes + blockDuration: 3600000, // 1 hour + skipSuccessfulRequests: true, + }; + + expect(() => RateLimitConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = RateLimitConfigSchema.parse(config); + + expect(result.enabled).toBe(true); + expect(result.maxAttempts).toBe(5); + expect(result.windowMs).toBe(900000); // 15 minutes + }); +}); + +describe('CSRFConfigSchema', () => { + it('should accept valid CSRF configuration', () => { + const config = { + enabled: true, + tokenLength: 64, + cookieName: 'csrf-token', + headerName: 'X-CSRF-Token', + }; + + expect(() => CSRFConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = CSRFConfigSchema.parse(config); + + expect(result.enabled).toBe(true); + expect(result.tokenLength).toBe(32); + expect(result.cookieName).toBe('csrf_token'); + }); +}); + +describe('AccountLinkingConfigSchema', () => { + it('should accept valid account linking configuration', () => { + const config = { + enabled: true, + autoLink: true, + requireVerification: false, + }; + + expect(() => AccountLinkingConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = {}; + const result = AccountLinkingConfigSchema.parse(config); + + expect(result.enabled).toBe(true); + expect(result.autoLink).toBe(false); + expect(result.requireVerification).toBe(true); + }); +}); + +describe('TwoFactorConfigSchema', () => { + it('should accept valid 2FA configuration', () => { + const config = { + enabled: true, + issuer: 'ObjectStack', + qrCodeSize: 256, + backupCodes: { + enabled: true, + count: 8, + }, + }; + + expect(() => TwoFactorConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values', () => { + const config = { enabled: false }; + const result = TwoFactorConfigSchema.parse(config); + + expect(result.qrCodeSize).toBe(200); + }); +}); + +describe('UserFieldMappingSchema', () => { + it('should accept custom field mappings', () => { + const config = { + id: 'user_id', + email: 'user_email', + name: 'full_name', + emailVerified: 'is_email_verified', + }; + + expect(() => UserFieldMappingSchema.parse(config)).not.toThrow(); + }); + + it('should use default field names', () => { + const config = {}; + const result = UserFieldMappingSchema.parse(config); + + expect(result.id).toBe('id'); + expect(result.email).toBe('email'); + expect(result.name).toBe('name'); + expect(result.emailVerified).toBe('email_verified'); + }); +}); + +describe('DatabaseAdapterSchema', () => { + it('should accept valid database adapter configuration', () => { + const config = { + type: 'prisma' as const, + connectionString: 'postgresql://localhost:5432/db', + tablePrefix: 'auth_', + schema: 'public', + }; + + expect(() => DatabaseAdapterSchema.parse(config)).not.toThrow(); + }); + + it('should accept all supported adapter types', () => { + const types = ['prisma', 'drizzle', 'kysely', 'custom'] as const; + + types.forEach((type) => { + const config = { type }; + expect(() => DatabaseAdapterSchema.parse(config)).not.toThrow(); + }); + }); + + it('should use default table prefix', () => { + const config = { type: 'drizzle' as const }; + const result = DatabaseAdapterSchema.parse(config); + + expect(result.tablePrefix).toBe('auth_'); + }); +}); + +describe('AuthPluginConfigSchema', () => { + it('should accept valid plugin configuration', () => { + const config = { + name: 'organization', + enabled: true, + options: { + maxOrganizations: 5, + allowInvites: true, + }, + }; + + expect(() => AuthPluginConfigSchema.parse(config)).not.toThrow(); + }); +}); + +describe('AuthConfigSchema', () => { + it('should accept minimal valid configuration', () => { + const config: AuthConfig = { + name: 'main_auth', + label: 'Main Authentication', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), // 32 character secret + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => AuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept comprehensive configuration', () => { + const config: AuthConfig = { + name: 'main_auth', + label: 'Main Authentication', + strategies: ['email_password', 'oauth', 'magic_link'], + baseUrl: 'https://app.example.com', + secret: 'super-secret-key-with-at-least-32-characters', + + emailPassword: { + enabled: true, + requireEmailVerification: true, + minPasswordLength: 10, + requirePasswordComplexity: true, + allowPasswordReset: true, + passwordResetExpiry: 7200, + }, + + magicLink: { + enabled: true, + expiryTime: 900, + }, + + oauth: { + providers: [ + { + provider: 'google', + clientId: 'google-client-id', + clientSecret: 'google-client-secret', + scopes: ['openid', 'profile', 'email'], + enabled: true, + }, + { + provider: 'github', + clientId: 'github-client-id', + clientSecret: 'github-client-secret', + enabled: true, + }, + ], + }, + + session: { + expiresIn: 604800, + cookieName: 'app-session', + cookieSecure: true, + cookieSameSite: 'lax', + }, + + rateLimit: { + enabled: true, + maxAttempts: 5, + windowMs: 900000, + }, + + csrf: { + enabled: true, + tokenLength: 32, + }, + + accountLinking: { + enabled: true, + autoLink: false, + requireVerification: true, + }, + + twoFactor: { + enabled: true, + issuer: 'My App', + }, + + userFieldMapping: {}, + + database: { + type: 'prisma', + tablePrefix: 'auth_', + }, + + plugins: [], + + active: true, + allowRegistration: true, + }; + + expect(() => AuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should enforce snake_case for name field', () => { + const invalidConfig = { + name: 'mainAuth', // camelCase - invalid + label: 'Main Auth', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => AuthConfigSchema.parse(invalidConfig)).toThrow(); + + const validConfig = { + ...invalidConfig, + name: 'main_auth', // snake_case - valid + }; + + expect(() => AuthConfigSchema.parse(validConfig)).not.toThrow(); + }); + + it('should require at least one strategy', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: [], // empty - invalid + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => AuthConfigSchema.parse(config)).toThrow(); + }); + + it('should require secret to be at least 32 characters', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'short', // too short + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => AuthConfigSchema.parse(config)).toThrow(); + }); + + it('should validate baseUrl is a valid URL', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'not-a-url', // invalid URL + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + expect(() => AuthConfigSchema.parse(config)).toThrow(); + }); + + it('should accept configuration with hooks', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + hooks: { + beforeSignIn: async ({ email }: { email: string }) => { + console.log('Before sign in:', email); + }, + afterSignIn: async ({ user, session }: { user: any; session: any }) => { + console.log('After sign in:', user.id); + }, + }, + }; + + expect(() => AuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept configuration with security settings', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + security: { + allowedOrigins: ['https://app.example.com'], + trustProxy: true, + ipRateLimiting: true, + sessionFingerprinting: true, + maxSessions: 3, + }, + }; + + expect(() => AuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept configuration with email settings', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + email: { + from: 'noreply@example.com', + fromName: 'My App', + provider: 'sendgrid' as const, + config: { + apiKey: 'sendgrid-api-key', + }, + }, + }; + + expect(() => AuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept configuration with UI customization', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + ui: { + brandName: 'My Brand', + logo: 'https://example.com/logo.png', + primaryColor: '#007bff', + customCss: '.button { color: blue; }', + }, + }; + + expect(() => AuthConfigSchema.parse(config)).not.toThrow(); + }); + + it('should use default values for optional fields', () => { + const config = { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + }; + + const result = AuthConfigSchema.parse(config); + + expect(result.active).toBe(true); + expect(result.allowRegistration).toBe(true); + expect(result.plugins).toEqual([]); + }); +}); + +describe('StandardAuthProviderSchema', () => { + it('should accept valid authentication provider', () => { + const provider: AuthenticationProvider = { + type: 'standard_auth', + config: { + name: 'main_auth', + label: 'Main Auth', + strategies: ['email_password', 'oauth'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + oauth: { + providers: [ + { + provider: 'google', + clientId: 'test-id', + clientSecret: 'test-secret', + }, + ], + }, + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }, + }; + + expect(() => StandardAuthProviderSchema.parse(provider)).not.toThrow(); + }); + + it('should require type to be "standard_auth"', () => { + const provider = { + type: 'other_auth', // invalid + config: { + name: 'test', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }, + }; + + expect(() => StandardAuthProviderSchema.parse(provider)).toThrow(); + }); +}); + +describe('Type inference', () => { + it('should correctly infer AuthenticationConfig type', () => { + const config: AuthConfig = { + name: 'test_auth', + label: 'Test Auth', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }; + + // This test passes if TypeScript compiles without errors + expect(config.name).toBe('test_auth'); + expect(config.strategies).toContain('email_password'); + }); + + it('should correctly infer StandardAuthProvider type', () => { + const provider: AuthenticationProvider = { + type: 'standard_auth', + config: { + name: 'test_auth', + label: 'Test', + strategies: ['email_password'], + baseUrl: 'https://example.com', + secret: 'a'.repeat(32), + session: {}, + rateLimit: {}, + csrf: {}, + accountLinking: {}, + }, + }; + + // This test passes if TypeScript compiles without errors + expect(provider.type).toBe('standard_auth'); + expect(provider.config.name).toBe('test_auth'); + }); +}); diff --git a/packages/spec/src/system/auth.zod.ts b/packages/spec/src/system/auth.zod.ts new file mode 100644 index 000000000..e00b12d90 --- /dev/null +++ b/packages/spec/src/system/auth.zod.ts @@ -0,0 +1,492 @@ +import { z } from 'zod'; + +/** + * Authentication Protocol + * + * Defines the standard authentication specification for the ObjectStack ecosystem. + * This protocol supports multiple authentication strategies, session management, + * and comprehensive security features. + * + * This is a framework-agnostic specification that can be implemented with any + * authentication library (better-auth, Auth.js, Passport, etc.) + */ + +/** + * Supported authentication strategies + */ +export const AuthStrategy = z.enum([ + 'email_password', // Traditional email & password authentication + 'magic_link', // Passwordless email magic link + 'oauth', // OAuth2 providers (Google, GitHub, etc.) + 'passkey', // WebAuthn / FIDO2 passkeys + 'otp', // One-time password (SMS, Email) + 'anonymous', // Anonymous/guest sessions +]); + +export type AuthStrategy = z.infer; + +/** + * OAuth Provider Configuration + * Supports popular OAuth2 providers + */ +export const OAuthProviderSchema = z.object({ + provider: z.enum([ + 'google', + 'github', + 'facebook', + 'twitter', + 'linkedin', + 'microsoft', + 'apple', + 'discord', + 'gitlab', + 'custom', + ]).describe('OAuth provider type'), + + clientId: z.string().describe('OAuth client ID'), + clientSecret: z.string().describe('OAuth client secret (typically from ENV)'), + + scopes: z.array(z.string()).optional().describe('Requested OAuth scopes'), + + redirectUri: z.string().url().optional().describe('OAuth callback URL'), + + enabled: z.boolean().default(true).describe('Whether this provider is enabled'), + + displayName: z.string().optional().describe('Display name for the provider button'), + + icon: z.string().optional().describe('Icon URL or identifier'), +}); + +export type OAuthProvider = z.infer; + +/** + * Email & Password Strategy Configuration + */ +export const EmailPasswordConfigSchema = z.object({ + enabled: z.boolean().default(true), + + requireEmailVerification: z.boolean().default(true).describe('Require email verification before login'), + + minPasswordLength: z.number().min(6).max(128).default(8).describe('Minimum password length'), + + requirePasswordComplexity: z.boolean().default(true).describe('Require uppercase, lowercase, numbers, symbols'), + + allowPasswordReset: z.boolean().default(true).describe('Enable password reset functionality'), + + passwordResetExpiry: z.number().default(3600).describe('Password reset token expiry in seconds'), +}); + +export type EmailPasswordConfig = z.infer; + +/** + * Magic Link Strategy Configuration + */ +export const MagicLinkConfigSchema = z.object({ + enabled: z.boolean().default(true), + + expiryTime: z.number().default(900).describe('Magic link expiry time in seconds (default 15 min)'), + + sendEmail: z.function() + .args(z.object({ + to: z.string().email(), + link: z.string().url(), + token: z.string(), + })) + .returns(z.promise(z.void())) + .optional() + .describe('Custom email sending function'), +}); + +export type MagicLinkConfig = z.infer; + +/** + * Passkey (WebAuthn) Strategy Configuration + */ +export const PasskeyConfigSchema = z.object({ + enabled: z.boolean().default(false), + + rpName: z.string().describe('Relying Party name'), + + rpId: z.string().optional().describe('Relying Party ID (defaults to domain)'), + + allowedOrigins: z.array(z.string().url()).optional().describe('Allowed origins for WebAuthn'), + + userVerification: z.enum(['required', 'preferred', 'discouraged']).default('preferred'), + + attestation: z.enum(['none', 'indirect', 'direct', 'enterprise']).default('none'), +}); + +export type PasskeyConfig = z.infer; + +/** + * Session Configuration + */ +export const SessionConfigSchema = z.object({ + expiresIn: z.number().default(86400 * 7).describe('Session expiry in seconds (default 7 days)'), + + updateAge: z.number().default(86400).describe('Session update interval in seconds (default 1 day)'), + + cookieName: z.string().default('session_token').describe('Session cookie name'), + + cookieSecure: z.boolean().default(true).describe('Use secure cookies (HTTPS only)'), + + cookieSameSite: z.enum(['strict', 'lax', 'none']).default('lax').describe('SameSite cookie attribute'), + + cookieDomain: z.string().optional().describe('Cookie domain'), + + cookiePath: z.string().default('/').describe('Cookie path'), + + cookieHttpOnly: z.boolean().default(true).describe('HttpOnly cookie attribute'), +}); + +export type SessionConfig = z.infer; + +/** + * Rate Limiting Configuration + */ +export const RateLimitConfigSchema = z.object({ + enabled: z.boolean().default(true), + + maxAttempts: z.number().default(5).describe('Maximum login attempts'), + + windowMs: z.number().default(900000).describe('Time window in milliseconds (default 15 min)'), + + blockDuration: z.number().default(900000).describe('Block duration after max attempts in ms'), + + skipSuccessfulRequests: z.boolean().default(false).describe('Only count failed requests'), +}); + +export type RateLimitConfig = z.infer; + +/** + * CSRF Protection Configuration + */ +export const CSRFConfigSchema = z.object({ + enabled: z.boolean().default(true), + + tokenLength: z.number().default(32).describe('CSRF token length'), + + cookieName: z.string().default('csrf_token').describe('CSRF cookie name'), + + headerName: z.string().default('X-CSRF-Token').describe('CSRF header name'), +}); + +export type CSRFConfig = z.infer; + +/** + * Account Linking Configuration + * Allows linking multiple auth methods to a single user account + */ +export const AccountLinkingConfigSchema = z.object({ + enabled: z.boolean().default(true).describe('Allow account linking'), + + autoLink: z.boolean().default(false).describe('Automatically link accounts with same email'), + + requireVerification: z.boolean().default(true).describe('Require email verification before linking'), +}); + +export type AccountLinkingConfig = z.infer; + +/** + * Two-Factor Authentication (2FA) Configuration + */ +export const TwoFactorConfigSchema = z.object({ + enabled: z.boolean().default(false), + + issuer: z.string().optional().describe('TOTP issuer name'), + + qrCodeSize: z.number().default(200).describe('QR code size in pixels'), + + backupCodes: z.object({ + enabled: z.boolean().default(true), + count: z.number().default(10).describe('Number of backup codes to generate'), + }).optional(), +}); + +export type TwoFactorConfig = z.infer; + +/** + * User Field Mapping Configuration + * Maps authentication user fields to ObjectStack user object fields + */ +export const UserFieldMappingSchema = z.object({ + id: z.string().default('id').describe('User ID field'), + email: z.string().default('email').describe('Email field'), + name: z.string().default('name').describe('Name field'), + image: z.string().default('image').optional().describe('Profile image field'), + emailVerified: z.string().default('email_verified').describe('Email verification status field'), + createdAt: z.string().default('created_at').describe('Created timestamp field'), + updatedAt: z.string().default('updated_at').describe('Updated timestamp field'), +}); + +export type UserFieldMapping = z.infer; + +/** + * Database Adapter Configuration + */ +export const DatabaseAdapterSchema = z.object({ + type: z.enum(['prisma', 'drizzle', 'kysely', 'custom']).describe('Database adapter type'), + + connectionString: z.string().optional().describe('Database connection string'), + + tablePrefix: z.string().default('auth_').describe('Prefix for auth tables'), + + schema: z.string().optional().describe('Database schema name'), +}); + +export type DatabaseAdapter = z.infer; + +/** + * Authentication Plugin Configuration + * Extends authentication with additional features + */ +export const AuthPluginConfigSchema = z.object({ + name: z.string().describe('Plugin name'), + + enabled: z.boolean().default(true), + + options: z.record(z.any()).optional().describe('Plugin-specific options'), +}); + +export type AuthPluginConfig = z.infer; + +/** + * Complete Authentication Configuration Schema + * + * This is the main configuration object for authentication + * in an ObjectStack application. + * + * @example + * ```typescript + * const authConfig: AuthConfig = { + * name: 'main_auth', + * label: 'Main Authentication', + * strategies: ['email_password', 'oauth'], + * baseUrl: 'https://app.example.com', + * secret: process.env.AUTH_SECRET, + * driver: 'better-auth', // Optional, defaults to 'better-auth' + * emailPassword: { + * enabled: true, + * minPasswordLength: 8, + * }, + * oauth: { + * providers: [{ + * provider: 'google', + * clientId: process.env.GOOGLE_CLIENT_ID, + * clientSecret: process.env.GOOGLE_CLIENT_SECRET, + * }], + * }, + * session: { + * expiresIn: 604800, // 7 days + * }, + * }; + * ``` + */ +export const AuthConfigSchema = z.object({ + /** + * Unique identifier for this auth configuration + * Must be in snake_case following ObjectStack conventions + */ + name: z.string() + .regex(/^[a-z_][a-z0-9_]*$/) + .describe('Configuration name (snake_case)'), + + /** + * Human-readable label + */ + label: z.string().describe('Display label'), + + /** + * The underlying authentication implementation driver + * Default: 'better-auth' (the reference implementation) + * Can be: 'better-auth', 'auth-js', 'passport', or custom driver name + */ + driver: z.string().default('better-auth').describe('The underlying authentication implementation driver'), + + /** + * Enabled authentication strategies + */ + strategies: z.array(AuthStrategy).min(1).describe('Enabled authentication strategies'), + + /** + * Base URL for the application + */ + baseUrl: z.string().url().describe('Application base URL'), + + /** + * Secret key for signing tokens and cookies + * Should be loaded from environment variables + */ + secret: z.string().min(32).describe('Secret key for signing (min 32 chars)'), + + /** + * Email & Password configuration + */ + emailPassword: EmailPasswordConfigSchema.optional(), + + /** + * Magic Link configuration + */ + magicLink: MagicLinkConfigSchema.optional(), + + /** + * Passkey (WebAuthn) configuration + */ + passkey: PasskeyConfigSchema.optional(), + + /** + * OAuth configuration + */ + oauth: z.object({ + providers: z.array(OAuthProviderSchema).min(1), + }).optional(), + + /** + * Session configuration + */ + session: SessionConfigSchema.default({}), + + /** + * Rate limiting configuration + */ + rateLimit: RateLimitConfigSchema.default({}), + + /** + * CSRF protection configuration + */ + csrf: CSRFConfigSchema.default({}), + + /** + * Account linking configuration + */ + accountLinking: AccountLinkingConfigSchema.default({}), + + /** + * Two-factor authentication configuration + */ + twoFactor: TwoFactorConfigSchema.optional(), + + /** + * User field mapping + */ + userFieldMapping: UserFieldMappingSchema.default({}), + + /** + * Database adapter configuration + */ + database: DatabaseAdapterSchema.optional(), + + /** + * Additional authentication plugins + */ + plugins: z.array(AuthPluginConfigSchema).default([]), + + /** + * Custom hooks for authentication events + */ + hooks: z.object({ + beforeSignIn: z.function() + .args(z.object({ email: z.string() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called before user sign in'), + + afterSignIn: z.function() + .args(z.object({ user: z.any(), session: z.any() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called after user sign in'), + + beforeSignUp: z.function() + .args(z.object({ email: z.string(), name: z.string().optional() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called before user registration'), + + afterSignUp: z.function() + .args(z.object({ user: z.any() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called after user registration'), + + beforeSignOut: z.function() + .args(z.object({ sessionId: z.string() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called before user sign out'), + + afterSignOut: z.function() + .args(z.object({ sessionId: z.string() })) + .returns(z.promise(z.void())) + .optional() + .describe('Called after user sign out'), + }).optional().describe('Authentication lifecycle hooks'), + + /** + * Advanced security settings + */ + security: z.object({ + allowedOrigins: z.array(z.string()).optional().describe('CORS allowed origins'), + + trustProxy: z.boolean().default(false).describe('Trust proxy headers'), + + ipRateLimiting: z.boolean().default(true).describe('Enable IP-based rate limiting'), + + sessionFingerprinting: z.boolean().default(true).describe('Enable session fingerprinting'), + + maxSessions: z.number().default(5).describe('Maximum concurrent sessions per user'), + }).optional().describe('Advanced security settings'), + + /** + * Email configuration for transactional emails + */ + email: z.object({ + from: z.string().email().describe('From email address'), + + fromName: z.string().optional().describe('From name'), + + provider: z.enum(['smtp', 'sendgrid', 'mailgun', 'ses', 'resend', 'custom']).describe('Email provider'), + + config: z.record(z.any()).optional().describe('Provider-specific configuration'), + }).optional().describe('Email configuration'), + + /** + * UI customization options + */ + ui: z.object({ + brandName: z.string().optional().describe('Brand name displayed in auth UI'), + + logo: z.string().optional().describe('Logo URL'), + + primaryColor: z.string().optional().describe('Primary brand color (hex)'), + + customCss: z.string().optional().describe('Custom CSS for auth pages'), + }).optional().describe('UI customization'), + + /** + * Whether this auth provider is active + */ + active: z.boolean().default(true).describe('Whether this provider is active'), + + /** + * Whether to allow new user registration + */ + allowRegistration: z.boolean().default(true).describe('Allow new user registration'), +}); + +/** + * TypeScript type inferred from AuthConfigSchema + */ +export type AuthConfig = z.infer; + +/** + * Standard Authentication Provider Schema + * Wraps the configuration for use in the identity system + */ +export const StandardAuthProviderSchema = z.object({ + type: z.literal('standard_auth').describe('Provider type identifier'), + + config: AuthConfigSchema.describe('Standard authentication configuration'), +}); + +export type StandardAuthProvider = z.infer;