Sorcha Tenant Service
Version: 2.0.0 Status: 97% Complete Framework: .NET 10.0 Architecture: Microservice
Overview
The Sorcha Tenant Service is a multi-tenant authentication, authorization, and organization management service that acts as a Secure Token Service (STS) for the Sorcha platform. It enables organizations to bring their own identity providers via OIDC federation, supports local email/password authentication with TOTP 2FA, and provides comprehensive organization administration capabilities.
Key Features
- Multi-Organization Support: Each organization has its own identity provider configuration, subdomain, and user management
- OIDC Identity Federation: Integrate with Microsoft Entra ID, Google, Okta, Apple, Amazon Cognito, or any OIDC-compliant provider with automatic discovery
- Full Token Exchange: External IDP tokens are exchanged for Sorcha JWTs; downstream services never see external tokens
- Local Authentication: Email/password login with NIST-compliant password policy and HIBP breach list checking
- TOTP Two-Factor Authentication: Authenticator app-based 2FA with backup codes
- Self-Registration: Public organizations can allow users to self-register with email verification
- PassKey Authentication: FIDO2/WebAuthn passkey authentication — org user 2FA (register + verify as second factor) and public user primary auth (signup, sign-in, method management)
- Server-Rendered Auth Pages: Razor Pages for login, signup, logout, OAuth/OIDC callbacks, email verification, and password reset — eliminates ~15MB WASM download for unauthenticated users
- Service-to-Service Authentication: OAuth2 client credentials flow for microservice communication
- JWT Token Issuance: RS256-signed tokens with configurable lifetimes
- Token Revocation: Redis-backed token blacklist with automatic TTL cleanup
- Multi-Tenant Data Isolation: PostgreSQL schema-based tenant isolation
- Organization Invitations: Invite users by email with configurable roles and expiry
- Domain Restrictions: Restrict auto-provisioning to specific email domains
- Custom Domain Support: Organizations can configure custom domains with CNAME verification
- Consolidated Roles: 5 roles (SystemAdmin, Administrator, Designer, Auditor, Member)
- User Lifecycle Management: Unlock, suspend, reactivate, and role change operations
- Admin Dashboard: Aggregated KPIs including user counts, role distribution, and login statistics
- Audit Logging: Comprehensive audit trail with configurable retention (1-120 months)
- Rate Limiting & Progressive Lockout: 5 fails=5min, 10=30min, 15=24h, 25=admin unlock
- Email Verification: Required for all users; trusts IDP
email_verifiedclaim for OIDC users - Multi-Tenant URL Resolution: 3-tier URL routing (path, subdomain, custom domain)
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Sorcha Tenant Service │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌───────────────┐ ┌────────────────┐ │
│ │ Auth API │ │ Admin API │ │ Audit API │ │
│ │ │ │ │ │ │ │
│ │ • OIDC SSO │ │ • Org Mgmt │ │ • Log Query │ │
│ │ • Local Auth │ │ • IDP Config │ │ • Retention │ │
│ │ • TOTP 2FA │ │ • User Mgmt │ │ • Dashboard │ │
│ │ • PassKey │ │ • Invitations │ │ │ │
│ │ • Token Mgmt │ │ • Domains │ │ │ │
│ └──────────────┘ └───────────────┘ └────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Server-Rendered Auth Pages (Razor) │ │
│ │ /auth/login /auth/signup /auth/logout │ │
│ │ /auth/social/callback /auth/oidc/callback │ │
│ │ /auth/verify-email /auth/reset-password │ │
│ │ /auth/error │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Service Layer │ │
│ │ • OrganizationService • TokenService │ │
│ │ • OidcExchangeService • OidcProvisioningService │ │
│ │ • IdpConfigurationService • TotpService │ │
│ │ • InvitationService • CustomDomainService │ │
│ │ • PasswordPolicyService • EmailVerificationService │ │
│ │ • DashboardService • PassKeyService │ │
│ │ • PublicUserService • LoginService │ │
│ │ • RegistrationService • PasswordResetService │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Data Layer │ │
│ │ • EF Core (PostgreSQL) • Redis Cache │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ PostgreSQL │ │ Redis │ │ External IDP │
│ (Multi- │ │ (Revoke │ │ (Azure/AWS/ │
│ tenant) │ │ List) │ │ Google) │
└──────────────┘ └─────────────┘ └──────────────┘Platform Identity Layer (Feature 058)
Authentication happens at the platform level via PlatformUser, while authorisation is scoped per-org via UserIdentity.
| Layer | Schema | Purpose | Entity |
|---|---|---|---|
| Platform | public | Authentication, cross-org anchor | PlatformUser |
| Organisation | org_ | Authorisation, org-scoped role | UserIdentity |
Key entities:
PlatformUser— Cross-org identity with email uniqueness, social logins, passkey credentialsPlatformSocialLogin— OAuth provider links (Google, GitHub, Microsoft, Apple)PlatformUserOrgMembership— Maps platform users to org-scoped rolesPlatformSettings— Platform governance (public org enable/disable, max orgs per user)
Well-known organisations:
- System Admin Org (
00000000-0000-0000-0000-000000000001) — Platform governance - Public Org (
00000000-0000-0000-0000-000000000002) — Social login + email/password signup
Quick Start
Prerequisites
- .NET 10 SDK - Download
- Docker Desktop - For PostgreSQL and Redis
- Git - Version control
1. Clone and Navigate
cd C:\Projects\Sorcha2. Set Up Local Secrets
Option A: Automated Setup (Recommended)
# Windows (PowerShell)
.\specs\001-tenant-auth\setup-local-secrets.ps1
# macOS/Linux (Bash)
chmod +x ./specs/001-tenant-auth/setup-local-secrets.sh
./specs/001-tenant-auth/setup-local-secrets.shOption B: Manual Setup
# Initialize User Secrets
dotnet user-secrets init --project src/Services/Sorcha.Tenant.Service
# Generate and set JWT signing key (see secrets-setup.md)
openssl genrsa -out jwt_private.pem 4096
dotnet user-secrets set "JwtSettings:SigningKey" "$(cat jwt_private.pem)" --project src/Services/Sorcha.Tenant.Service
# Set database password
dotnet user-secrets set "ConnectionStrings:Password" "dev_password123" --project src/Services/Sorcha.Tenant.ServiceFor detailed secrets management, see the Authentication Setup guide in docs/guides/AUTHENTICATION-SETUP.md.
3. Start Dependencies
# Start PostgreSQL and Redis
docker-compose up -d postgres redis4. Run Database Migrations
cd src/Services/Sorcha.Tenant.Service
dotnet ef database update5. Run the Service
dotnet runService will start at:
- HTTPS: https://localhost:7080
- HTTP: http://localhost:7081
- Scalar API Docs: https://localhost:7080/scalar
Configuration
appsettings.json Structure
{
"ConnectionStrings": {
"TenantDatabase": "Host=localhost;Port=5432;Database=sorcha_tenant;Username=sorcha_user;Password=placeholder"
},
"Redis": {
"ConnectionString": "localhost:6379",
"InstanceName": "SorchaTenant:"
},
"JwtSettings": {
"Issuer": "https://localhost:7080",
"Audience": ["https://localhost:7081"],
"AccessTokenLifetimeMinutes": 60,
"RefreshTokenLifetimeMinutes": 1440
},
"Fido2": {
"ServerDomain": "localhost",
"ServerName": "Sorcha Tenant Service"
},
"EmailSettings": {
"SmtpHost": "localhost",
"SmtpPort": 587,
"SmtpUser": "",
"SmtpPassword": "",
"FromAddress": "noreply@sorcha.example.com",
"FromName": "Sorcha Platform",
"EnableSsl": true
},
"OidcSettings": {
"CallbackBaseUrl": "https://localhost:7080",
"StateTokenLifetimeMinutes": 10,
"LoginTokenLifetimeMinutes": 5
}
}New Configuration Settings (054)
| Section | Key | Default | Purpose |
|---|---|---|---|
EmailSettings:SmtpHost | — | SMTP server hostname | |
EmailSettings:SmtpPort | 587 | SMTP server port | |
EmailSettings:SmtpUser | — | SMTP authentication username | |
EmailSettings:SmtpPassword | — | SMTP authentication password (use secrets) | |
EmailSettings:FromAddress | — | Sender email address | |
EmailSettings:FromName | Sorcha Platform | Sender display name | |
EmailSettings:EnableSsl | true | Enable TLS/SSL for SMTP | |
OidcSettings:CallbackBaseUrl | — | Base URL for OIDC callback redirects | |
OidcSettings:StateTokenLifetimeMinutes | 10 | OIDC state token expiry | |
OidcSettings:LoginTokenLifetimeMinutes | 5 | 2FA login token expiry | |
Fido2:ServerDomain | localhost | WebAuthn relying party domain | |
Fido2:ServerName | Sorcha Tenant Service | WebAuthn display name |
Environment Variables
For production deployment, use environment variables:
ConnectionStrings__TenantDatabase="Host=prod-db;Port=5432;..."
Redis__ConnectionString="prod-redis:6379"
JwtSettings__Issuer="https://api.sorcha.example.com"
AzureKeyVault__Enabled="true"
AzureKeyVault__VaultUri="https://sorcha-kv.vault.azure.net/"
EmailSettings__SmtpHost="smtp.example.com"
EmailSettings__SmtpPassword="your-smtp-password"
EmailSettings__FromAddress="noreply@sorcha.example.com"
OidcSettings__CallbackBaseUrl="https://api.sorcha.example.com"API Endpoints
Authentication API (/api/auth)
| Endpoint | Method | Description |
|---|---|---|
/api/auth/login | POST | Login with email and password (returns 2FA challenge if enabled) |
/api/auth/verify-2fa | POST | Verify TOTP code or backup code to complete login |
/api/auth/register | POST | Self-register with email/password (public orgs only) |
/api/auth/logout | POST | Logout and revoke current token |
/api/auth/me | GET | Get current authenticated user info |
/api/auth/token/refresh | POST | Refresh access token |
/api/auth/token/revoke | POST | Revoke a specific token |
/api/auth/token/introspect | POST | Introspect a token (service-to-service) |
/api/auth/token/revoke-user | POST | Revoke all tokens for a user (admin) |
/api/auth/token/revoke-organization | POST | Revoke all tokens for an organization (admin) |
OIDC Authentication API (/api/auth)
| Endpoint | Method | Description |
|---|---|---|
/api/auth/oidc/initiate | POST | Initiate OIDC login flow (generates authorization URL) |
/api/auth/callback/{orgSubdomain} | GET | OIDC callback - exchange authorization code for Sorcha JWT |
/api/auth/oidc/complete-profile | POST | Complete user profile after OIDC provisioning |
/api/auth/verify-email | POST | Verify email address with token |
/api/auth/resend-verification | POST | Resend email verification (rate limited: 3/hour) |
Organisation Switching
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/auth/me/organizations | List user's org memberships | Authenticated |
| POST | /api/auth/switch-org | Switch active org (re-issues JWT) | Authenticated |
Org User PassKey 2FA API (/api/passkey)
| Endpoint | Method | Description |
|---|---|---|
/api/passkey/register/options | POST | Get passkey registration options for org user (authenticated) |
/api/passkey/register/verify | POST | Complete passkey registration for org user |
/api/passkey/credentials | GET | List org user's passkey credentials |
/api/passkey/credentials/{id} | DELETE | Delete/revoke an org user's passkey credential |
Org User PassKey 2FA Login (/api/auth)
| Endpoint | Method | Description |
|---|---|---|
/api/auth/verify-passkey/options | POST | Get passkey assertion options for 2FA verification during login |
/api/auth/verify-passkey | POST | Verify passkey assertion to complete 2FA login |
Public User Passkey API (/api/auth/public/passkey) — Anonymous, Rate-Limited
| Endpoint | Method | Description |
|---|---|---|
/api/auth/public/passkey/register/options | POST | Create PlatformUser + generate FIDO2 registration options for public signup |
/api/auth/public/passkey/register/verify | POST | Verify attestation, create UserIdentity in public org, issue JWT |
Public User Passkey Sign-in (/api/auth/passkey) — Anonymous, Rate-Limited
| Endpoint | Method | Description |
|---|---|---|
/api/auth/passkey/assertion/options | POST | Generate discoverable passkey assertion options (optional email filter) |
/api/auth/passkey/assertion/verify | POST | Verify assertion, resolve user, issue JWT |
Public User Social Login (/api/auth/public/social)
| Endpoint | Method | Description |
|---|---|---|
/api/auth/public/social/initiate | POST | Initiate social login/signup with provider (Google, Microsoft, GitHub, Apple) |
/api/auth/public/social/callback | POST | Handle OAuth callback and issue tokens |
Browser callback URL: providers redirect to the Razor page at /auth/social/callback (single canonical path per environment, see docs/guides/SOCIAL-LOGIN-SETUP.md). The page resolves the provider from the cached state — provider is NOT a query parameter — and applies the strict link policy added in feature 115 before issuing tokens.
Strict link policy (feature 115). Both signup and link flows refuse when verification is missing on either side:
- New user creation requires the provider to assert
email_verified=true. Otherwise → refusal withprovider_unverifiedreason. - Cross-method linking (existing Sorcha account, new social provider with the same email) requires both the provider's claim and the existing account's
EmailVerifiedto be true. Otherwise → refusal withexisting_unverifiedreason. - Returning users (provider+sub already linked) are NOT re-checked against verification gates — trust is established at link time.
Provider visibility. Signup and login pages render a "Continue with..." button only for providers configured with non-empty ClientId and ClientSecret. Configuration shape:
SocialProviders__0__Name: Google
SocialProviders__0__ClientId: ${GOOGLE_OAUTH_CLIENT_ID}
SocialProviders__0__ClientSecret: ${GOOGLE_OAUTH_CLIENT_SECRET}Adding a new provider requires only configuration + service restart. See docs/guides/SOCIAL-LOGIN-SETUP.md for the operator runbook.
Telemetry. Refusals emit sorcha_social_login_refusal_total{provider, reason} on the Sorcha.Tenant meter. PII is never tagged on these metrics; the matching log line carries a hash-based redacted email tag.
Public-organisation seed. A fresh database can be seeded with PublicOrgEnabled=true via PlatformSettings__SeedPublicOrgEnabled=true (feature 115 FR-019). Seed-time only — admin UI/API toggles win on subsequent boots.
Public User Auth Method Management (/api/auth/public) — Authenticated
| Endpoint | Method | Description |
|---|---|---|
/api/auth/public/methods | GET | List authenticated user's passkeys and social links |
/api/auth/public/social/link | POST | Link a social account to existing user |
/api/auth/public/social/{linkId} | DELETE | Unlink a social account (enforces last-method guard) |
/api/auth/public/passkey/add/options | POST | Get options for adding a passkey to existing account |
/api/auth/public/passkey/add/verify | POST | Complete adding a passkey to existing account |
Organization API (/api/organizations)
| Endpoint | Method | Description |
|---|---|---|
/api/organizations | POST | Create a new organization |
/api/organizations | GET | List organizations (admin) |
/api/organizations/{id} | GET | Get organization details |
/api/organizations/{id} | PUT | Update organization (admin) |
/api/organizations/{id} | DELETE | Deactivate organization (admin, soft delete) |
/api/organizations/by-subdomain/{subdomain} | GET | Get organization by subdomain (public) |
/api/organizations/validate-subdomain/{subdomain} | GET | Validate subdomain availability (public) |
/api/organizations/stats | GET | Get organization statistics (public) |
User Management API (/api/organizations/{orgId}/users)
| Endpoint | Method | Description |
|---|---|---|
/api/organizations/{orgId}/users | POST | Add user to organization (admin) |
/api/organizations/{orgId}/users | GET | List organization users |
/api/organizations/{orgId}/users/{userId} | GET | Get user details |
/api/organizations/{orgId}/users/{userId} | PUT | Update user (admin) |
/api/organizations/{orgId}/users/{userId} | DELETE | Remove user from organization (admin) |
/api/organizations/{orgId}/users/{userId}/unlock | POST | Unlock a locked user account (admin) |
/api/organizations/{orgId}/users/{userId}/suspend | POST | Suspend a user account (admin) |
/api/organizations/{orgId}/users/{userId}/reactivate | POST | Reactivate a suspended account (admin) |
/api/organizations/{orgId}/users/{userId}/role | PUT | Change a user's role (admin) |
/api/organizations/{orgId}/users/{userId}/verify-email | POST | Admin override to mark email as verified (admin) |
User List Query Parameters (GET /api/organizations/{orgId}/users):
includeInactive(bool) — Include suspended/deleted usersemailVerified(bool?) — Filter by email verification statusprovisionedVia(string?) — Filter by provisioning method (Local, Oidc, Invitation, etc.)includePending(bool) — Include pending OrgInvitation records
Enhanced UserResponse now includes: EmailVerified, EmailVerifiedAt, ProvisionedVia, InvitedByUserId, ProfileCompleted, InvitationStatus.
IDP Configuration API (/api/organizations/{orgId}/idp)
| Endpoint | Method | Description |
|---|---|---|
/api/organizations/{orgId}/idp | GET | Get IDP configuration |
/api/organizations/{orgId}/idp | PUT | Create or update IDP configuration |
/api/organizations/{orgId}/idp | DELETE | Delete IDP configuration |
/api/organizations/{orgId}/idp/discover | POST | Discover OIDC endpoints from issuer URL |
/api/organizations/{orgId}/idp/test | POST | Test IDP connection (client_credentials grant) |
/api/organizations/{orgId}/idp/toggle | POST | Enable or disable IDP |
Supported provider presets: Microsoft Entra, Google, Okta, Apple, Amazon Cognito, Generic OIDC
Invitation API (/api/organizations/{orgId}/invitations)
| Endpoint | Method | Description |
|---|---|---|
/api/organizations/{orgId}/invitations | POST | Send an organization invitation (admin) |
/api/organizations/{orgId}/invitations | GET | List invitations (filter by status) |
/api/organizations/{orgId}/invitations/{id}/revoke | POST | Revoke a pending invitation (admin) |
Invitation details: 32-byte cryptographic token, configurable expiry (1-30 days, default 7). Invited users bypass domain restrictions.
Domain Restrictions API (/api/organizations/{orgId}/domain-restrictions)
| Endpoint | Method | Description |
|---|---|---|
/api/organizations/{orgId}/domain-restrictions | GET | Get allowed email domains for auto-provisioning |
/api/organizations/{orgId}/domain-restrictions | PUT | Update allowed email domains (admin) |
Note: An empty array disables restrictions (all domains allowed).
TOTP Two-Factor Authentication API (/api/totp)
| Endpoint | Method | Description |
|---|---|---|
/api/totp/setup | POST | Initiate TOTP setup (generates secret, QR URI, backup codes) |
/api/totp/verify | POST | Verify initial TOTP code to complete enrollment |
/api/totp/validate | POST | Validate TOTP code during login (uses loginToken) |
/api/totp/backup-validate | POST | Validate and consume a one-time backup code |
/api/totp | DELETE | Disable TOTP 2FA |
/api/totp/status | GET | Get TOTP 2FA status |
Rate limiting: TOTP validation endpoints use the totp-validate policy. Limit configurable via RateLimiting:TotpPermitLimit in appsettings.json (production recommendation: 5/min per IP).
Organization Settings API (/api/organizations/{orgId}/settings)
| Endpoint | Method | Description |
|---|---|---|
/api/organizations/{orgId}/settings | GET | Get org settings (type, self-registration, domains, audit retention) |
/api/organizations/{orgId}/settings | PUT | Update settings (self-registration, audit retention 1-120 months) |
Custom Domain API (/api/organizations/{orgId}/custom-domain)
| Endpoint | Method | Description |
|---|---|---|
/api/organizations/{orgId}/custom-domain | GET | Get custom domain configuration and verification status |
/api/organizations/{orgId}/custom-domain | PUT | Configure custom domain (returns CNAME instructions) |
/api/organizations/{orgId}/custom-domain | DELETE | Remove custom domain configuration |
/api/organizations/{orgId}/custom-domain/verify | POST | Verify custom domain CNAME DNS resolution |
Admin Dashboard API (/api/organizations/{orgId}/dashboard)
| Endpoint | Method | Description |
|---|---|---|
/api/organizations/{orgId}/dashboard | GET | Get admin dashboard KPIs (user counts, roles, logins, invitations, IDP status) |
Audit API (/api/organizations/{orgId}/audit)
| Endpoint | Method | Description |
|---|---|---|
/api/organizations/{orgId}/audit | GET | Query audit events (paginated, filterable by date/type/user) |
/api/organizations/{orgId}/audit/retention | GET | Get audit retention configuration |
/api/organizations/{orgId}/audit/retention | PUT | Update audit retention period (1-120 months) |
Max page size: 200 events. Audit events older than the retention period are automatically purged daily.
Platform Organisation Management
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/platform/organizations | List all organisations (paginated, status filter) | SystemAdmin |
| PUT | /api/platform/organizations/{orgId}/status | Update org status (Active/Suspended) | SystemAdmin |
| GET | /api/platform/organizations/{orgId}/users | List org users (read-only audit) | SystemAdmin |
| POST | /api/platform/organizations | Create org with admin invite | SystemAdmin |
| GET | /api/platform/settings | Get platform settings | SystemAdmin |
| PUT | /api/platform/settings/public-org | Enable/disable public org | SystemAdmin |
Organization Recovery Configuration API (/api/organizations/{orgId}/recovery-config)
| Endpoint | Method | Description |
|---|---|---|
/api/organizations/{orgId}/recovery-config | POST | Create or update organization recovery configuration (admin) |
/api/organizations/{orgId}/recovery-config | GET | Get organization recovery configuration |
OrgRecoveryConfig entity: Stores the organization's recovery public key and policy settings for organization-delegated wallet recovery (Feature 060). Administrators configure this to enable org-level wallet recovery for their users.
The Tenant Service also exposes passkey public key data used by the Wallet Service's PasskeyServiceClient during passkey-based wallet recovery key wrapping.
Internal API (/api/internal)
| Endpoint | Method | Description |
|---|---|---|
/api/internal/resolve-domain/{domain} | GET | Resolve custom domain to organization subdomain (API Gateway use only) |
Note: Internal endpoints are excluded from public API documentation.
For full API documentation, open Scalar UI at https://localhost:7080/scalar.
Address Lookup (Feature 103)
The Tenant Service hosts the postcode → address autofill API used by the PostcodeLookupRenderer form control. Providers are pluggable behind IAddressLookupProvider:
| Provider | Capability | Country | Auth | Default |
|---|---|---|---|---|
| Postcodes.io | ValidateOnly (postcode → town / region / country / lat-long) | UK | None (free public API) | ✅ Always on |
| OS Places | FullAddress (postcode → candidate list) | UK | API key | ❌ Opt-in |
Endpoints (routed via API Gateway /api/*)
GET /api/address-lookup/providers— list configured providers and their live availabilityPOST /api/address-lookup/postcode— resolve a postcode (validate-only metadata or full-address candidate list)
Both require a Bearer JWT and apply the standard API rate-limit policy. The renderer falls back to plain text when no provider is reachable, so service downtime never blocks form submission.
Configuration
{
"AddressLookup": {
"Enabled": true,
"Providers": {
"PostcodesIo": { "Enabled": true },
"OSPlaces": { "Enabled": false, "ApiKey": "" }
},
"CacheTtlMinutes": 60
}
}To enable OS Places, set Providers.OSPlaces.Enabled = true and supply an API key obtained from os.uk/datahub. Provider order in the config sets preference: the first provider with capability FullAddress wins over any ValidateOnly provider for the same postcode.
Development
Project Structure
src/Services/Sorcha.Tenant.Service/
├── Endpoints/ # Minimal API endpoint groups
│ ├── AuthEndpoints.cs # Login, register, logout, token management
│ ├── OidcEndpoints.cs # OIDC initiate, callback, profile, email verification
│ ├── OrganizationEndpoints.cs # Org CRUD, user management, lifecycle
│ ├── IdpConfigurationEndpoints.cs # IDP CRUD, discover, test, toggle
│ ├── InvitationEndpoints.cs # Create, list, revoke invitations
│ ├── DomainRestrictionEndpoints.cs # Email domain restrictions
│ ├── TotpEndpoints.cs # TOTP 2FA setup, verify, validate, backup
│ ├── OrgSettingsEndpoints.cs # Org settings management
│ ├── CustomDomainEndpoints.cs # Custom domain CNAME management
│ ├── DashboardEndpoints.cs # Admin dashboard KPIs
│ ├── AuditEndpoints.cs # Audit log query and retention
│ ├── InternalEndpoints.cs # Domain resolution (API Gateway internal)
│ ├── BootstrapEndpoints.cs # Initial system bootstrap (creates System Admin Org + Public Org, PlatformUser for admin, PlatformSettings)
│ ├── ServiceAuthEndpoints.cs # Service-to-service auth
│ ├── PasskeyEndpoints.cs # Org user passkey registration and 2FA
│ ├── PublicAuthEndpoints.cs # Public user passkey, social login, method management
│ ├── ParticipantEndpoints.cs # Participant identity management
│ ├── PushSubscriptionEndpoints.cs # Push notification subscriptions
│ └── UserPreferenceEndpoints.cs # User preference management
├── Services/ # Business logic services
│ ├── OrganizationService.cs
│ ├── TokenService.cs
│ ├── TotpService.cs
│ ├── IdpConfigurationService.cs
│ ├── OidcExchangeService.cs
│ ├── OidcProvisioningService.cs
│ ├── InvitationService.cs
│ ├── CustomDomainService.cs
│ ├── DashboardService.cs
│ ├── PasswordPolicyService.cs
│ ├── EmailVerificationService.cs
│ ├── PassKeyService.cs
│ ├── PublicUserService.cs
│ └── ...
├── Data/ # Data access layer
│ ├── TenantDbContext.cs
│ ├── Repositories/
│ │ ├── IOrganizationRepository.cs
│ │ ├── IIdentityRepository.cs
│ │ ├── ICustomDomainRepository.cs
│ │ └── ...
│ └── Migrations/
├── Models/ # Domain models and DTOs
│ ├── Dtos/ # Request/response DTOs
│ ├── UserIdentity.cs
│ ├── Organization.cs
│ ├── IdentityProviderConfiguration.cs
│ ├── Invitation.cs
│ ├── CustomDomainMapping.cs
│ ├── AuditLogEntry.cs
│ └── ...
├── Extensions/ # Service extensions
│ ├── ServiceCollectionExtensions.cs
│ └── ApplicationBuilderExtensions.cs
├── appsettings.json
├── appsettings.Development.json
└── Program.csRunning Tests
# Unit tests
dotnet test tests/Sorcha.Tenant.Service.Tests
# Integration tests (uses Testcontainers)
dotnet test tests/Sorcha.Tenant.Service.IntegrationTests
# Performance tests
dotnet run --project tests/Sorcha.Tenant.Service.PerformanceTestsCode Coverage
dotnet test --collect:"XPlat Code Coverage"
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"coverage" -reporttypes:HtmlDatabase Migrations
# Create new migration
dotnet ef migrations add MigrationName --context TenantDbContext
# Apply migrations
dotnet ef database update
# Revert migration
dotnet ef database update PreviousMigrationName
# Generate SQL script
dotnet ef migrations script --output migrations.sqlSecurity Considerations
Secrets Management
- Local Development: Use .NET User Secrets (stored outside project directory)
- Production: Use Azure Key Vault, AWS Secrets Manager, or HashiCorp Vault
- NEVER commit secrets to source control
JWT Signing Keys
- Algorithm: RS256 (RSA-SHA256) with 4096-bit keys
- Rotation: Rotate keys every 90 days
- Storage: Private key in Key Vault, public key in JWKS endpoint
Multi-Tenancy
- Data Isolation: PostgreSQL schemas per organization (
org_{id}) - Row-Level Security: EF Core query filters prevent cross-tenant data access
- Audit Logging: All operations logged with organization context
Password Policy (NIST SP 800-63B)
- Minimum Length: 12 characters
- No Complexity Rules: No forced uppercase/numbers/symbols
- Breach List Check: Validates against HIBP (Have I Been Pwned) database
- BCrypt Hashing: Passwords stored as BCrypt hashes
Two-Factor Authentication
- TOTP: Time-based One-Time Password (RFC 6238) via authenticator apps
- Backup Codes: 8-character alphanumeric one-time recovery codes
- Login Flow: Password verification issues a short-lived loginToken, then TOTP validation issues full JWT
Rate Limiting & Progressive Lockout
- Login Attempts: Progressive lockout (5 fails=5min, 10=30min, 15=24h, 25=permanent admin unlock)
- Token Requests: 100 requests per minute per client
- Admin Operations: 20 requests per minute per user
- TOTP Validation: 5 attempts per minute per user/IP
- Email Verification Resend: 3 per hour per user
Transactional Email Architecture (Feature 112)
All transactional email the Tenant Service sends — verification, invitation, password reset, and welcome — goes through a single templated pipeline. The entry point application code uses is ITransactionalEmailService.
Caller (EmailVerificationService / InvitationService / PasswordResetService /
WelcomeEmailDispatcher)
│
▼ typed dispatch record
ITransactionalEmailService ← the only surface callers touch
│
├── IEmailTemplateRenderer (Scriban, embedded resources, pre-parsed at startup)
│ │
│ ▼
│ Emails/Templates/*.html + .txt (six pairs; base.* is shared layout)
│
└── IEmailSender (SMTP via MailKit OR Azure Communication Services)
multipart HTML + plaintext on every messageTemplates live under Emails/Templates/ as embedded resources:
| Name | Purpose | Branding |
|---|---|---|
base.html / base.txt | Shared frame (logo/sender header, body, footer with reply-to) | — |
verify.html / .txt | Confirm-your-email after email+password signup | Sorcha |
invite.html / .txt | Organisation invitation, clear org name + role | Per-org (logo, colour) |
reset.html / .txt | Password reset link | Sorcha |
welcome-public.html / .txt | First-verify greeting with recovery-phrase advance warning | Sorcha |
welcome-invited.html / .txt | First-login greeting for org-invited users | Per-org |
Branding is resolved per-send by IEmailBrandingResolver. Invitations and invited welcomes pull Organization.Name / Branding.LogoUrl / Branding.PrimaryColor from the inviting org with per-field fallback to Sorcha platform defaults — the org name always wins; any branding field missing on the org falls back to Sorcha's default.
Welcome email is one-shot per user. WelcomeEmailDispatcher.SendIfPendingAsync is called from three trigger points and is idempotent (guarded by PlatformUser.WelcomeSentAt) and non-throwing (a send failure is logged but never reverses the authentication flow):
EmailVerificationService.VerifyTokenAsync— afterEmailVerified = trueon the email+password signup pathLoginService— after a successful password login (covers users who've already verified and are logging in for the first time)SocialCallbackRazor PageModel — after successful social-login OAuth exchange (social users skip verification; the IdP pre-verified the email)
Variant selection is based on the user's PlatformUserOrgMembership rows: public-org-only → welcome-public (with recovery-phrase advance-warning section); any standard-org membership → welcome-invited with the earliest- joined standard org as the "inviting" org.
Design-history reference: docs/superpowers/specs/2026-04-24-email-sweep-design.md carries the full design rationale. The feature spec, plan, tasks, and contracts live under specs/112-email-sweep/.
Snapshot fixtures for every template pair are committed under tests/Sorcha.Tenant.Service.Tests/Fixtures/Emails/. When a deliberate copy change is made to a template, regenerate fixtures with UPDATE_EMAIL_FIXTURES=1 dotnet test --filter "~EmailTemplateSnapshotTests".
Tone and content guardrails
- Single clear action per message (one CTA button).
- No recovery-phrase content in any email body. The public welcome primes users for the recovery-phrase moment at wallet creation but never includes phrase material — the phrase is shown exactly once in the UI at wallet creation and is never stored.
- No phishing-shaped language. Every email footer includes a reply-to.
Authorization Roles
The Tenant Service uses 5 consolidated roles for access control:
| Role | Description | Key Permissions |
|---|---|---|
| SystemAdmin | Platform-level administrator | Full access, cannot be assigned via API |
| Administrator | Organization administrator | IDP config, user management, invitations, settings, dashboard |
| Designer | Blueprint designer | Create/manage blueprints and workflows |
| Auditor | Compliance/audit reviewer | Read-only access to audit logs and reports |
| Member | Standard organization member | Basic access, participate in workflows |
Authorization Policies
| Policy | Required Role(s) |
|---|---|
RequireAdministrator | SystemAdmin or Administrator |
RequireAuditor | SystemAdmin, Administrator, or Auditor |
RequireOrganizationMember | Any authenticated organization member |
RequireService | Service-to-service tokens only |
OIDC Integration Flow
The service implements a full authorization code + PKCE exchange flow:
- Initiate (
POST /api/auth/oidc/initiate): Client sends org subdomain, receives authorization URL - Redirect: User is redirected to the external IDP (Microsoft Entra, Google, etc.)
- Callback (
GET /api/auth/callback/{orgSubdomain}): IDP redirects back with authorization code - Exchange: Service exchanges code for external tokens, validates ID token
- Provision: Auto-provisions new users or matches existing users
- JWT Issuance: Issues Sorcha JWT (downstream services never see external tokens)
- 2FA Check: If TOTP is enabled, returns a loginToken for second-factor validation
- Profile Completion: If required claims are missing, prompts for profile completion
Provider Presets
The IDP configuration supports auto-discovery and presets for top providers:
| Provider | Preset Name | Discovery URL Pattern |
|---|---|---|
| Microsoft Entra ID | MicrosoftEntra | https://login.microsoftonline.com/{tenantId}/v2.0 |
Google | https://accounts.google.com | |
| Okta | Okta | https://{domain}.okta.com |
| Apple | Apple | https://appleid.apple.com |
| Amazon Cognito | AmazonCognito | https://cognito-idp.{region}.amazonaws.com/{poolId} |
| Generic OIDC | GenericOidc | Any .well-known/openid-configuration URL |
Multi-Tenant URL Resolution
The service supports 3-tier URL resolution for organizations:
| Tier | Pattern | Example |
|---|---|---|
| Path | /org/{subdomain} | https://sorcha.dev/org/acme |
| Subdomain | {subdomain}.sorcha.dev | https://acme.sorcha.dev |
| Custom Domain | CNAME to platform | https://id.acme.com |
Custom domains require CNAME DNS configuration and verification. The internal /api/internal/resolve-domain/{domain} endpoint is used by the API Gateway for domain-based routing.
Deployment
.NET Aspire (Development)
# Run via Aspire orchestration
dotnet run --project src/Apps/Sorcha.AppHost
# Aspire Dashboard: http://localhost:15888Docker
# Build image
docker build -t sorcha-tenant-service -f src/Services/Sorcha.Tenant.Service/Dockerfile .
# Run container
docker run -p 7080:8080 \
-e ConnectionStrings__TenantDatabase="Host=db;..." \
-e Redis__ConnectionString="redis:6379" \
sorcha-tenant-serviceAzure App Service
# Deploy via Azure CLI
az webapp create --name sorcha-tenant-service --resource-group sorcha-rg --plan sorcha-plan
az webapp deployment source config-zip --name sorcha-tenant-service --resource-group sorcha-rg --src publish.zipObservability
Logging (Serilog + OTLP)
- Structured Logging: Serilog with machine name, thread ID, application enrichment
- Correlation IDs: Track requests across services
- Aspire Dashboard: Centralized log viewer via OTLP (http://localhost:18888)
// Example log entry
Log.Information("User {UserId} authenticated for organization {OrgId}", userId, orgId);Tracing (OpenTelemetry + Zipkin)
- Distributed Tracing: End-to-end request tracing
- Zipkin Dashboard: http://localhost:9411
Metrics (Prometheus)
- Metrics Endpoint:
/metrics - Custom Metrics: Login success/failure rates, token issuance latency
Troubleshooting
Database Connection Issues
Error: "Connection refused" or "password authentication failed"
Solution:
# Check PostgreSQL is running
docker ps | grep postgres
# Verify User Secrets
dotnet user-secrets list --project src/Services/Sorcha.Tenant.Service
# Test connection
psql -h localhost -U sorcha_user -d sorcha_tenant_devRedis Connection Issues
Error: "It was not possible to connect to the redis server(s)"
Solution:
# Check Redis is running
docker ps | grep redis
# Test connection
redis-cli ping # Should return: PONGToken Validation Failures
Error: "Invalid signature" or "Token has expired"
Solution:
- Ensure JWT signing key is configured in User Secrets
- Check system clock synchronization (token validation uses timestamps)
- Verify JWKS endpoint is accessible:
https://localhost:7080/.well-known/jwks.json
Contributing
Development Workflow
- Create Feature Branch:
git checkout -b feature/your-feature - Write Tests First: Follow TDD (Test-Driven Development)
- Implement Feature: Follow existing code patterns
- Run Tests: Ensure all tests pass
- Update Documentation: Update README, API docs, specs
- Submit PR: Reference task ID in commit message
Code Standards
- C# Conventions: Follow Microsoft C# coding conventions
- Async/Await: Use async for all I/O operations
- Dependency Injection: Use constructor injection
- OpenAPI Documentation: All endpoints must have XML documentation
Resources
- Authentication Setup: docs/guides/AUTHENTICATION-SETUP.md
- Architecture: docs/reference/architecture.md
- Development Status: docs/reference/development-status.md
License
This project is licensed under the Apache License 2.0. See LICENSE for details.
Support
For issues, questions, or contributions:
- GitHub Issues: Sorcha Issues
- Documentation: Sorcha Docs
- CLAUDE.md: AI Assistant Guide
Last Updated: 2026-03-16 Maintained By: Sorcha Contributors Deferred (Post-MVD): Azure AD B2C integration