Skip to content

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_verified claim 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.

LayerSchemaPurposeEntity
PlatformpublicAuthentication, cross-org anchorPlatformUser
Organisationorg_Authorisation, org-scoped roleUserIdentity

Key entities:

  • PlatformUser — Cross-org identity with email uniqueness, social logins, passkey credentials
  • PlatformSocialLogin — OAuth provider links (Google, GitHub, Microsoft, Apple)
  • PlatformUserOrgMembership — Maps platform users to org-scoped roles
  • PlatformSettings — 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

bash
cd C:\Projects\Sorcha

2. Set Up Local Secrets

Option A: Automated Setup (Recommended)

bash
# 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.sh

Option B: Manual Setup

bash
# 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.Service

For detailed secrets management, see the Authentication Setup guide in docs/guides/AUTHENTICATION-SETUP.md.

3. Start Dependencies

bash
# Start PostgreSQL and Redis
docker-compose up -d postgres redis

4. Run Database Migrations

bash
cd src/Services/Sorcha.Tenant.Service
dotnet ef database update

5. Run the Service

bash
dotnet run

Service will start at:


Configuration

appsettings.json Structure

json
{
  "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)

SectionKeyDefaultPurpose
EmailSettings:SmtpHostSMTP server hostname
EmailSettings:SmtpPort587SMTP server port
EmailSettings:SmtpUserSMTP authentication username
EmailSettings:SmtpPasswordSMTP authentication password (use secrets)
EmailSettings:FromAddressSender email address
EmailSettings:FromNameSorcha PlatformSender display name
EmailSettings:EnableSsltrueEnable TLS/SSL for SMTP
OidcSettings:CallbackBaseUrlBase URL for OIDC callback redirects
OidcSettings:StateTokenLifetimeMinutes10OIDC state token expiry
OidcSettings:LoginTokenLifetimeMinutes52FA login token expiry
Fido2:ServerDomainlocalhostWebAuthn relying party domain
Fido2:ServerNameSorcha Tenant ServiceWebAuthn display name

Environment Variables

For production deployment, use environment variables:

bash
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)

EndpointMethodDescription
/api/auth/loginPOSTLogin with email and password (returns 2FA challenge if enabled)
/api/auth/verify-2faPOSTVerify TOTP code or backup code to complete login
/api/auth/registerPOSTSelf-register with email/password (public orgs only)
/api/auth/logoutPOSTLogout and revoke current token
/api/auth/meGETGet current authenticated user info
/api/auth/token/refreshPOSTRefresh access token
/api/auth/token/revokePOSTRevoke a specific token
/api/auth/token/introspectPOSTIntrospect a token (service-to-service)
/api/auth/token/revoke-userPOSTRevoke all tokens for a user (admin)
/api/auth/token/revoke-organizationPOSTRevoke all tokens for an organization (admin)

OIDC Authentication API (/api/auth)

EndpointMethodDescription
/api/auth/oidc/initiatePOSTInitiate OIDC login flow (generates authorization URL)
/api/auth/callback/{orgSubdomain}GETOIDC callback - exchange authorization code for Sorcha JWT
/api/auth/oidc/complete-profilePOSTComplete user profile after OIDC provisioning
/api/auth/verify-emailPOSTVerify email address with token
/api/auth/resend-verificationPOSTResend email verification (rate limited: 3/hour)

Organisation Switching

MethodPathDescriptionAuth
GET/api/auth/me/organizationsList user's org membershipsAuthenticated
POST/api/auth/switch-orgSwitch active org (re-issues JWT)Authenticated

Org User PassKey 2FA API (/api/passkey)

EndpointMethodDescription
/api/passkey/register/optionsPOSTGet passkey registration options for org user (authenticated)
/api/passkey/register/verifyPOSTComplete passkey registration for org user
/api/passkey/credentialsGETList org user's passkey credentials
/api/passkey/credentials/{id}DELETEDelete/revoke an org user's passkey credential

Org User PassKey 2FA Login (/api/auth)

EndpointMethodDescription
/api/auth/verify-passkey/optionsPOSTGet passkey assertion options for 2FA verification during login
/api/auth/verify-passkeyPOSTVerify passkey assertion to complete 2FA login

Public User Passkey API (/api/auth/public/passkey) — Anonymous, Rate-Limited

EndpointMethodDescription
/api/auth/public/passkey/register/optionsPOSTCreate PlatformUser + generate FIDO2 registration options for public signup
/api/auth/public/passkey/register/verifyPOSTVerify attestation, create UserIdentity in public org, issue JWT

Public User Passkey Sign-in (/api/auth/passkey) — Anonymous, Rate-Limited

EndpointMethodDescription
/api/auth/passkey/assertion/optionsPOSTGenerate discoverable passkey assertion options (optional email filter)
/api/auth/passkey/assertion/verifyPOSTVerify assertion, resolve user, issue JWT

Public User Social Login (/api/auth/public/social)

EndpointMethodDescription
/api/auth/public/social/initiatePOSTInitiate social login/signup with provider (Google, Microsoft, GitHub, Apple)
/api/auth/public/social/callbackPOSTHandle 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 with provider_unverified reason.
  • Cross-method linking (existing Sorcha account, new social provider with the same email) requires both the provider's claim and the existing account's EmailVerified to be true. Otherwise → refusal with existing_unverified reason.
  • 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:

yaml
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

EndpointMethodDescription
/api/auth/public/methodsGETList authenticated user's passkeys and social links
/api/auth/public/social/linkPOSTLink a social account to existing user
/api/auth/public/social/{linkId}DELETEUnlink a social account (enforces last-method guard)
/api/auth/public/passkey/add/optionsPOSTGet options for adding a passkey to existing account
/api/auth/public/passkey/add/verifyPOSTComplete adding a passkey to existing account

Organization API (/api/organizations)

EndpointMethodDescription
/api/organizationsPOSTCreate a new organization
/api/organizationsGETList organizations (admin)
/api/organizations/{id}GETGet organization details
/api/organizations/{id}PUTUpdate organization (admin)
/api/organizations/{id}DELETEDeactivate organization (admin, soft delete)
/api/organizations/by-subdomain/{subdomain}GETGet organization by subdomain (public)
/api/organizations/validate-subdomain/{subdomain}GETValidate subdomain availability (public)
/api/organizations/statsGETGet organization statistics (public)

User Management API (/api/organizations/{orgId}/users)

EndpointMethodDescription
/api/organizations/{orgId}/usersPOSTAdd user to organization (admin)
/api/organizations/{orgId}/usersGETList organization users
/api/organizations/{orgId}/users/{userId}GETGet user details
/api/organizations/{orgId}/users/{userId}PUTUpdate user (admin)
/api/organizations/{orgId}/users/{userId}DELETERemove user from organization (admin)
/api/organizations/{orgId}/users/{userId}/unlockPOSTUnlock a locked user account (admin)
/api/organizations/{orgId}/users/{userId}/suspendPOSTSuspend a user account (admin)
/api/organizations/{orgId}/users/{userId}/reactivatePOSTReactivate a suspended account (admin)
/api/organizations/{orgId}/users/{userId}/rolePUTChange a user's role (admin)
/api/organizations/{orgId}/users/{userId}/verify-emailPOSTAdmin override to mark email as verified (admin)

User List Query Parameters (GET /api/organizations/{orgId}/users):

  • includeInactive (bool) — Include suspended/deleted users
  • emailVerified (bool?) — Filter by email verification status
  • provisionedVia (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)

EndpointMethodDescription
/api/organizations/{orgId}/idpGETGet IDP configuration
/api/organizations/{orgId}/idpPUTCreate or update IDP configuration
/api/organizations/{orgId}/idpDELETEDelete IDP configuration
/api/organizations/{orgId}/idp/discoverPOSTDiscover OIDC endpoints from issuer URL
/api/organizations/{orgId}/idp/testPOSTTest IDP connection (client_credentials grant)
/api/organizations/{orgId}/idp/togglePOSTEnable or disable IDP

Supported provider presets: Microsoft Entra, Google, Okta, Apple, Amazon Cognito, Generic OIDC

Invitation API (/api/organizations/{orgId}/invitations)

EndpointMethodDescription
/api/organizations/{orgId}/invitationsPOSTSend an organization invitation (admin)
/api/organizations/{orgId}/invitationsGETList invitations (filter by status)
/api/organizations/{orgId}/invitations/{id}/revokePOSTRevoke 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)

EndpointMethodDescription
/api/organizations/{orgId}/domain-restrictionsGETGet allowed email domains for auto-provisioning
/api/organizations/{orgId}/domain-restrictionsPUTUpdate allowed email domains (admin)

Note: An empty array disables restrictions (all domains allowed).

TOTP Two-Factor Authentication API (/api/totp)

EndpointMethodDescription
/api/totp/setupPOSTInitiate TOTP setup (generates secret, QR URI, backup codes)
/api/totp/verifyPOSTVerify initial TOTP code to complete enrollment
/api/totp/validatePOSTValidate TOTP code during login (uses loginToken)
/api/totp/backup-validatePOSTValidate and consume a one-time backup code
/api/totpDELETEDisable TOTP 2FA
/api/totp/statusGETGet 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)

EndpointMethodDescription
/api/organizations/{orgId}/settingsGETGet org settings (type, self-registration, domains, audit retention)
/api/organizations/{orgId}/settingsPUTUpdate settings (self-registration, audit retention 1-120 months)

Custom Domain API (/api/organizations/{orgId}/custom-domain)

EndpointMethodDescription
/api/organizations/{orgId}/custom-domainGETGet custom domain configuration and verification status
/api/organizations/{orgId}/custom-domainPUTConfigure custom domain (returns CNAME instructions)
/api/organizations/{orgId}/custom-domainDELETERemove custom domain configuration
/api/organizations/{orgId}/custom-domain/verifyPOSTVerify custom domain CNAME DNS resolution

Admin Dashboard API (/api/organizations/{orgId}/dashboard)

EndpointMethodDescription
/api/organizations/{orgId}/dashboardGETGet admin dashboard KPIs (user counts, roles, logins, invitations, IDP status)

Audit API (/api/organizations/{orgId}/audit)

EndpointMethodDescription
/api/organizations/{orgId}/auditGETQuery audit events (paginated, filterable by date/type/user)
/api/organizations/{orgId}/audit/retentionGETGet audit retention configuration
/api/organizations/{orgId}/audit/retentionPUTUpdate 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

MethodPathDescriptionAuth
GET/api/platform/organizationsList all organisations (paginated, status filter)SystemAdmin
PUT/api/platform/organizations/{orgId}/statusUpdate org status (Active/Suspended)SystemAdmin
GET/api/platform/organizations/{orgId}/usersList org users (read-only audit)SystemAdmin
POST/api/platform/organizationsCreate org with admin inviteSystemAdmin
GET/api/platform/settingsGet platform settingsSystemAdmin
PUT/api/platform/settings/public-orgEnable/disable public orgSystemAdmin

Organization Recovery Configuration API (/api/organizations/{orgId}/recovery-config)

EndpointMethodDescription
/api/organizations/{orgId}/recovery-configPOSTCreate or update organization recovery configuration (admin)
/api/organizations/{orgId}/recovery-configGETGet 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)

EndpointMethodDescription
/api/internal/resolve-domain/{domain}GETResolve 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:

ProviderCapabilityCountryAuthDefault
Postcodes.ioValidateOnly (postcode → town / region / country / lat-long)UKNone (free public API)✅ Always on
OS PlacesFullAddress (postcode → candidate list)UKAPI key❌ Opt-in

Endpoints (routed via API Gateway /api/*)

  • GET /api/address-lookup/providers — list configured providers and their live availability
  • POST /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

json
{
  "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.cs

Running Tests

bash
# 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.PerformanceTests

Code Coverage

bash
dotnet test --collect:"XPlat Code Coverage"
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"coverage" -reporttypes:Html

Database Migrations

bash
# 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.sql

Security 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 message

Templates live under Emails/Templates/ as embedded resources:

NamePurposeBranding
base.html / base.txtShared frame (logo/sender header, body, footer with reply-to)
verify.html / .txtConfirm-your-email after email+password signupSorcha
invite.html / .txtOrganisation invitation, clear org name + rolePer-org (logo, colour)
reset.html / .txtPassword reset linkSorcha
welcome-public.html / .txtFirst-verify greeting with recovery-phrase advance warningSorcha
welcome-invited.html / .txtFirst-login greeting for org-invited usersPer-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):

  1. EmailVerificationService.VerifyTokenAsync — after EmailVerified = true on the email+password signup path
  2. LoginService — after a successful password login (covers users who've already verified and are logging in for the first time)
  3. SocialCallback Razor 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:

RoleDescriptionKey Permissions
SystemAdminPlatform-level administratorFull access, cannot be assigned via API
AdministratorOrganization administratorIDP config, user management, invitations, settings, dashboard
DesignerBlueprint designerCreate/manage blueprints and workflows
AuditorCompliance/audit reviewerRead-only access to audit logs and reports
MemberStandard organization memberBasic access, participate in workflows

Authorization Policies

PolicyRequired Role(s)
RequireAdministratorSystemAdmin or Administrator
RequireAuditorSystemAdmin, Administrator, or Auditor
RequireOrganizationMemberAny authenticated organization member
RequireServiceService-to-service tokens only

OIDC Integration Flow

The service implements a full authorization code + PKCE exchange flow:

  1. Initiate (POST /api/auth/oidc/initiate): Client sends org subdomain, receives authorization URL
  2. Redirect: User is redirected to the external IDP (Microsoft Entra, Google, etc.)
  3. Callback (GET /api/auth/callback/{orgSubdomain}): IDP redirects back with authorization code
  4. Exchange: Service exchanges code for external tokens, validates ID token
  5. Provision: Auto-provisions new users or matches existing users
  6. JWT Issuance: Issues Sorcha JWT (downstream services never see external tokens)
  7. 2FA Check: If TOTP is enabled, returns a loginToken for second-factor validation
  8. Profile Completion: If required claims are missing, prompts for profile completion

Provider Presets

The IDP configuration supports auto-discovery and presets for top providers:

ProviderPreset NameDiscovery URL Pattern
Microsoft Entra IDMicrosoftEntrahttps://login.microsoftonline.com/{tenantId}/v2.0
GoogleGooglehttps://accounts.google.com
OktaOktahttps://{domain}.okta.com
AppleApplehttps://appleid.apple.com
Amazon CognitoAmazonCognitohttps://cognito-idp.{region}.amazonaws.com/{poolId}
Generic OIDCGenericOidcAny .well-known/openid-configuration URL

Multi-Tenant URL Resolution

The service supports 3-tier URL resolution for organizations:

TierPatternExample
Path/org/{subdomain}https://sorcha.dev/org/acme
Subdomain{subdomain}.sorcha.devhttps://acme.sorcha.dev
Custom DomainCNAME to platformhttps://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)

bash
# Run via Aspire orchestration
dotnet run --project src/Apps/Sorcha.AppHost

# Aspire Dashboard: http://localhost:15888

Docker

bash
# 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-service

Azure App Service

bash
# 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.zip

Observability

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)
csharp
// Example log entry
Log.Information("User {UserId} authenticated for organization {OrgId}", userId, orgId);

Tracing (OpenTelemetry + Zipkin)

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:

bash
# 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_dev

Redis Connection Issues

Error: "It was not possible to connect to the redis server(s)"

Solution:

bash
# Check Redis is running
docker ps | grep redis

# Test connection
redis-cli ping  # Should return: PONG

Token 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

  1. Create Feature Branch: git checkout -b feature/your-feature
  2. Write Tests First: Follow TDD (Test-Driven Development)
  3. Implement Feature: Follow existing code patterns
  4. Run Tests: Ensure all tests pass
  5. Update Documentation: Update README, API docs, specs
  6. 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


License

This project is licensed under the Apache License 2.0. See LICENSE for details.


Support

For issues, questions, or contributions:


Last Updated: 2026-03-16 Maintained By: Sorcha Contributors Deferred (Post-MVD): Azure AD B2C integration

Released under the MIT License.