Skip to content
bitzorcas
中EN

Reference

Identity configuration, permissions, and integrations

Reference for passwords, client cipher Identity:Password:Cipher, JWT, key rotation, frontend links, MFA, federation, notifications, permissions, and production adapters.

Last updated

This page answers three production questions: which code reads a setting, what happens when it is absent, and how to prove that a real adapter has replaced the placeholder. A Host that starts from configuration is not necessarily commercially ready.

1. Registration order

Register Identity application services before Infrastructure adapters. TryAdd defaults allow later concrete registrations to replace them; incorrect order can leave a Null or Unavailable implementation active.

Recommended Host composition order
// 1. CoreRuntime registers Identity policy, enhanced-MFA entry, and fail-closed defaults.
services.AddBitzOrcasCoreRuntime(configuration);
// 2. With both database and RabbitMQ configuration, register generated stores,
// notification delivery, and federation. API Shell keeps CoreRuntime defaults.
services.AddBitzOrcasPersistenceAdapters(configuration);
// 3. Authentication schemes and API pipeline consume the closed Identity service graph.
services.AddBitzOrcasAuthentication(configuration, environment);
services.AddBitzOrcasApiPipeline(configuration);

AddBitzOrcasCoreRuntime calls AddBitzOrcasPlatformIdentity and AddBitzOrcasMfaConnectors; the production branch of AddBitzOrcasPersistenceAdapters registers notification connectors and AddBitzOrcasIdentityFederationConnectors. When creating a Host, copy the verified composition relationship rather than calling internal extensions twice.

2. Password settings

KeyDefaultMeaning
Identity:Password:RequireDigittrueAt least one digit
RequireLowercasetrueAt least one lowercase character
RequireUppercasetrueAt least one uppercase character
RequireNonAlphanumerictrueAt least one symbol
RequiredLength8Minimum characters
RequiredUniqueChars1Minimum distinct characters
Identity:Password:BCrypt:WorkFactor12BCrypt cost
Identity:Password:Expiry:PasswordExpiryDays900 means no expiry
PasswordExpiryWarningDays7Advance warning
Identity:Password:History:PasswordHistoryCount5Configured history count

2.1 Password client encryption Identity:Password:Cipher

Bound type: PasswordCipherOptions (PasswordCipherOptions.SectionName).

The product baseline requires RSA-OAEP-SHA256 client encryption with no plaintext password fallback. Omitting the section uses the defaults below; login still works because the SDK calls GET /api/auth/cipher-key and encrypts.

KeyDefaultValid / effective rangeOut-of-range behaviorMeaning
Identity:Password:Cipher:KeySize20482048–4096ValidateOnStart fails (process will not start); key creation also falls back to 2048 if still out of rangeRSA bits; distributed mode protects a PKCS#8 private-key snapshot, while fallback mode holds an in-process RSA
RotationHours241–720 (cap = 720 hours = 30 days)No startup validation; CipherKeyRotationHostedService applies Math.Clamp(..., 1, 24*30) silentlyRotation interval in hours. 0 becomes 1; 1000 becomes 720
GracePeriodMinutes10Runtime lower bound 1 minute; no startup upper boundMath.Max(1, GracePeriodMinutes)How long the previous key still decrypts. Keep much smaller than the rotation interval; recommend 5–30. SDK refreshes the public key once on KeyExpired
MaxClockSkewSeconds12030–600 (10 minutes)ValidateOnStart failsAllowed payload timestamp skew vs server clock; also returned on cipher-key for client clock correction
NonceTtlSeconds12030–600 (10 minutes)ValidateOnStart fails; claim path also Clamp(30, 600)One-time Nonce cache TTL (seconds); anti-replay
Password policy + client cipher in appsettings.Production.json
{
"Identity": {
"Password": {
"RequireDigit": true,
"RequireLowercase": true,
"RequireUppercase": true,
"RequireNonAlphanumeric": true,
"RequiredLength": 12,
"RequiredUniqueChars": 6,
"BCrypt": {
"WorkFactor": 12
},
"Expiry": {
"PasswordExpiryDays": 90,
"PasswordExpiryWarningDays": 7
},
"History": {
"PasswordHistoryCount": 5
},
"Cipher": {
"KeySize": 2048,
"RotationHours": 24,
"GracePeriodMinutes": 10,
"MaxClockSkewSeconds": 120,
"NonceTtlSeconds": 120
}
}
}
}

Validation vs runtime clamp (do not confuse)

FieldStartup ValidateOnStartRuntime
KeySizeYes (2048–4096)Key creation also falls back to 2048
RotationHoursNoClamp(1, 720); values above 720 still start the process but rotate every 30 days
GracePeriodMinutesNoAt least 1 minute
MaxClockSkewSeconds / NonceTtlSecondsYes (30–600)Nonce TTL clamped again when claimed

So if someone sets RotationHours: 8760 (one year) expecting fewer rotations, the host still starts, but rotation is capped at 720h (30 days). Treat this doc and the source Math.Clamp(..., 1, 24 * 30) as source of truth — not “whatever is written in appsettings”.

Key lifecycle and cipher-key response

ItemBehavior (matches source)
GenerationDistributed mode generates a shared first key when the cache key is absent; GetOrCreateAsync suppresses concurrent multi-instance factories. Only memory fallback generates at each process start
keyId{yyyyMMddHHmmss}-{6 hex}, e.g. 20260811120000-a1b2c3
Public keyRSA SubjectPublicKeyInfo (SPKI) Base64; algorithm id fixed RSA-OAEP-SHA256
Private keyDistributed mode immediately protects exported PKCS#8 with a dedicated Data Protection purpose and zeroes plaintext bytes; memory mode retains only an in-process RSA. Neither mode writes config or logs
Rotation cadenceCipherKeyRotationHostedService sleeps RotationHours first, then RotateNow() — not an immediate rotate on start. Current becomes previous; purged after GracePeriodMinutes
Ring sizeAt most current + previous decrypt; older keyIds → KeyExpired
Distributed cache keyidentity:cipher:keyring:v1; TTL covers rotation, grace, and a one-hour buffer with 5% jitter
GET /api/auth/cipher-keykeyId, publicKey, algorithm, serverTimestamp (UTC ms), maxClockSkewSeconds
Payloadnonce|timestamp|password then RSA-OAEP-SHA256; password may contain | (only the first two separators are used)
Nonce cache keyArea identity:cipher:nonce + nonce; prefers ICacheStore

Ops notes and current boundaries

  • Horizontal scale / multi-instance: when both ICacheStore and ICipherKeyMaterialProtector are available, the current + previous snapshot is shared in distributed cache. Instance B can decrypt ciphertext encrypted with a public key served by instance A. Every instance must share the same ASP.NET Core Data Protection key ring; otherwise private-key unprotection fails as KeyExpired.
  • Fallback mode: if either the cache or protector is absent, the service logs a Warning and uses an in-process ring. Cross-node requests can then still return KeyExpired. Fix production composition; use sticky sessions only as a temporary mitigation. The SDK’s single refresh retry absorbs a race but is not a scaling design.
  • Nonce storage prefers ICacheStore (e.g. Redis). Without it, the process falls back to an in-memory dictionary and logs a Warning — multi-instance replay protection is single-node only; use Redis in production.
  • Algorithm is fixed RSA-OAEP + SHA-256; payload/flow details: login security.
  • Raising KeySize (3072/4096) increases CPU and ciphertext size; load-test before shipping.
  • Large RotationHours (near 720) lengthens single-key exposure; very small values increase KeyExpired and SDK retries. Default 24 fits most deployments.
  • GracePeriodMinutes should cover “client cached public key → submit” latency and stay well below the rotation interval; multi-hour grace weakens rotation benefits.
  • Error codes: Identity.Cipher.KeyExpired, DecryptFailed, ClockSkewExceeded, ReplayDetected, EncryptedPasswordRequired, PayloadInvalid.

Benchmark login throughput and CPU on production-sized hardware before raising BCrypt cost. Do not lower it below security policy for throughput, and do not double it without capacity evidence.

3. JWT base settings

KeyDefaultProduction requirement
Jwt:IssuerBitzOrcasMatch validation side
Jwt:AudienceBitzOrcasMatch API resource
Jwt:SecretEmptyAt least 32 characters, supplied by secret management
Jwt:AccessTokenExpirationMinutes15Shorter increases refresh load; longer increases exposure
Jwt:RefreshTokenExpirationDays7Aligns with default seven-day session semantics

A single Secret is acceptable for development. Production should prefer SigningKeys with Kid, allowing issuance from the active key and validation with grace-period keys.

JWT key ring and rotation
{
"Jwt": {
"Issuer": "https://identity.example.com",
"Audience": "bitzorcas-api",
"AccessTokenExpirationMinutes": 15,
"RefreshTokenExpirationDays": 7,
"SigningKeys": [
{
"Kid": "identity-2026-q3",
"Secret": "secret-injected-by-managed-key-provider-at-runtime",
"ActiveFrom": "2026-07-01T00:00:00Z",
"ActiveTo": null
}
],
"Rotation": {
"Enabled": true,
"RotationIntervalDays": 90,
"GracePeriodDays": 7,
"CronExpression": "0 0 2 * * ?"
}
}
}

The Secret above is a placeholder. Never commit a real key. NullJwtKeyRotationSink only records rotation; managed-key persistence requires a production IJwtKeyRotationSink.

FrontendLinkBuilder reads Frontend:BaseUrl and the route dictionary. The default BaseUrl is http://localhost:6800 and is local-development only.

Production frontend links
{
"Frontend": {
"BaseUrl": "https://portal.example.com",
"Routes": {
"ResetPassword": "/reset-password",
"Activate": "/activate",
"VerifyEmail": "/verify-email",
"ConfirmPhone": "/confirm-phone"
}
}
}

BaseUrl must be HTTPS, fixed to a trusted host, and have no trailing slash. Routes are server configuration and must not accept an arbitrary ReturnUrl. Place notification tokens where the frontend can submit them immediately without exposing them to analytics, Referer, or logs.

5. Defaults and failure semantics

AddBitzOrcasPlatformIdentity registers:

  • BCryptPasswordHasher, DefaultPasswordValidator, and JwtTokenService.
  • LoginFlow, password-expiry, and password-history services.
  • Built-in TotpMfaService.
  • UnavailableAuthProvider for AzureAD, WeChat, WeCom, and DingTalk.
  • UnavailableWeChatMiniProgramAuthService.
  • NullEmailDeliveryPort and NullSmsDeliveryPort.
  • NullLoginLogArchiveQueryPort and NullJwtKeyRotationSink.

Unavailable federation should return a stable unavailable error. Null delivery is best effort. Neither should make a readiness probe claim that the related business capability is ready.

6. Federation adapters

After AddBitzOrcasIdentityFederationConnectors, each adapter registers independently:

ConnectorConditionConcrete adapter
SocialAuthSocialAuth section existsSocialAuthAdapter
Azure ADAzureAD:Enabled=true and ClientId presentAzureAdAdapter, plus Graph, JIT, and JwtBearer
LDAPLdap:IsEnabled=true and Host presentLdapAuthAdapter
WeChat mini programBoth AppId and AppSecret presentWeChatMiniProgramAdapter on its own port
Minimum shape that conditionally registers Azure AD
{
"AzureAD": {
"Enabled": true,
"Instance": "https://login.microsoftonline.com/",
"TenantId": "organization-tenant-id",
"ClientId": "application-client-id",
"ClientSecret": "injected-secret",
"CallbackPath": "/api/callback/azuread",
"Scopes": ["openid", "profile", "email"]
}
}

Application and Contracts must not reference connector packages. Vendor SDKs, HttpClient, and error translation stay in Infrastructure. A successful external identity still enters local account, tenant, role, and session semantics.

6.1 Tenant-level Provider management

Static Host configuration loads protocol adapters. Tenant runtime parameters and credentials are managed through an API and take effect without a process restart. Code-registered Profiles define allowed fields, defaults, interaction, and required Adapter; the tenant-owned ExternalLoginProviderConfiguration aggregate references only a stable ProfileId.

The current Profile catalog is:

ProfileIdProviderKeyInteractionRequired core fieldsWrite-only credentialJIT / connection test
azure-adAzureADRedirectTenantId, ClientId, Scopes containing openid, and UserIdClaimClientSecretBoth supported
social-oauth2SocialRedirectpublic HTTPS Authorization/Token/UserInfo endpoints, ClientId, scopes, and identity claimsClientSecretBoth supported
ldapLdapCredentialsHost, port 1–65535, SSL, SearchBaseDn, object class, 500–30000ms timeout, plus a user-DN template or UPN suffixBindPassword; required when BindDn is setBoth supported
wechat-mini-programWeChatMiniProgramMiniProgramAppIdAppSecretBoth supported

Social OAuth endpoints must be public HTTPS URLs with no UserInfo, query, or fragment. SAML has its own dynamic configuration, metadata validation, and runtime-snapshot lifecycle, so it is deliberately not duplicated in these four Profiles.

Management API and authorization

Method and pathBehaviorTransaction/concurrency
GET /api/external-login/configurationscode Profiles, Adapter load state, and redacted configuration for the current tenantstandard read timeout
PUT /api/external-login/configurations/{profileId}create/update display name, enabled state, JIT, complete parameters, and this request’s secret changesoptimistic Version; delegation-sensitive
DELETE /api/external-login/configurations/{profileId}?Version={version}versioned tenant soft deletemissing target is idempotent; delegation-sensitive
POST /api/external-login/configurations/{profileId}/testreal connection test using saved configurationINonTransactionalCommand, so network I/O holds no DB transaction

All four use cases require Manage on resource identity/externallogin, represented by permission identity.externallogin.manage. The frontend entry is the External Login sheet in Integration Center. It is a client of the management API, not a second configuration source of truth.

Parameters in a save request is the complete non-sensitive set. Secrets contains only values created or replaced in this request, while ClearedSecretKeys explicitly removes values. Existing credentials omitted from both are preserved. Reads return only ConfiguredSecretKeys; they never return credential values, SecretsJson, or its search hash. The persistence column uses the standard Data Protection sensitive-column pipeline. Runtime secrets do not enter responses, logs, exceptions, cache keys, or telemetry labels.

Before enabling a configuration, the service checks the Profile schema, required credentials, and whether a real Adapter is loaded. Stable readiness values are Ready, Disabled, Incomplete, AdapterUnavailable, and NotConfigured. A connection test uses saved configuration and returns only success, UTC time, latency, stable error code, and a redacted message.

Every public Provider listing, login initiation, callback, mini-program exchange, and connection test re-reads the current tenant’s authoritative Store; credentials are not cached. Rotation therefore takes effect immediately. Redirect flows also compare {configurationId}:{version} so a callback cannot continue under credentials changed after initiation. Explicit disable or delete removes the Provider from the public login list and does not fall back to legacy static configuration.

7. Enhanced MFA connector

Built-in TOTP remains available without the connector. With MFA:Enabled=true, AddBitzOrcasMfaConnectors adds email/SMS OTP, FIDO2, recovery codes, trusted devices, and risk-driven policy.

The default connector MfaCacheProvider is in process. A hosted service warns outside Development. Multi-instance production must replace it with Redis or another distributed implementation; otherwise a FIDO2 challenge generated on one instance may be verified on another and fail.

NullUserMfaConfigRepository is also a placeholder. When enhanced MFA is enabled, verify concrete user-MFA storage, email/SMS delivery, and challenge cache.

8. Notification adapters

Activation, password reset, email confirmation, and phone confirmation consume IEmailDeliveryPort or ISmsDeliveryPort. Map provider states to operating semantics:

Provider stateUse-case resultOperator action
AcceptedSuccess with message identifierTrack final receipt
Retryable timeoutFailure or reliable OutboxBounded retry and alert
Permanently invalid addressTyped business failureAsk user to correct address
Provider not configuredFail commercial flow closedRed readiness and block GA

The current Null port can let a call complete without delivery, so the GA gate must inspect the concrete implementation or run an end-to-end delivery probe.

9. Permission catalog

IdentityPermissions currently owns:

  • Users: list, read, create, update, delete, enable, disable, lock, unlock.
  • Roles: list, read, create, update, delete.
  • Login logs: list, read.
  • MFA: manage.
  • Devices: list, manage.
  • Sessions: list, manage.
  • External login: manage.
  • Organization units: list, read, create, update, delete.

Invitation, Admission, and Tenant commands also express resource authorization through ResourceDescriptor and AuthorizationAction. Catalog permissions and resource actions are complementary; neither alone is the full decision.

10. Sensitive configuration rules

  • Host-level JWT and adapter-bootstrap credentials come from secret stores or environment injection; tenant external-login credentials enter only through the management API and the protected SecretsJson sensitive column.
  • PlatformTenant.ConnectionString never enters normal configuration responses, logs, or audit detail.
  • Password hashes, security stamps, raw tokens, and FIDO2 challenges are not structured-log fields.
  • Configuration snapshots and diagnostic endpoints apply fixed masking rather than retaining prefixes and suffixes.
  • Rotation leaves old keys for validation grace but never continues issuing with retired keys.

11. Startup verification

Terminal window
# Inspect option binding and default implementations.
rg -n "Bind\(|TryAdd|Unavailable|Null.*Port|NullJwt" \
src/Platform/Identity/BitzOrcas.Identity.Application/Identity/DependencyInjection.cs
# Inspect actual connector conditions and replacement registrations.
rg -n "GetSection|GetValue|AddScoped<IExternalAuthProvider|MFA:Enabled" \
src/Platform/Identity/BitzOrcas.Identity.Infrastructure -g '*.cs'
# Committed appsettings should not contain an apparent real secret.
rg -n '"(Secret|ClientSecret|AppSecret|Password)"\s*:\s*"[^$<{]' \
src/Hosts -g 'appsettings*.json'

The last command is heuristic and requires review. It does not replace a secret scanner.

12. Production acceptance table

CheckPassing evidence
Password policyWeak-password and history-reuse integration tests
Key ringNew-Kid issuance, old-Kid grace validation, expired-key rejection
Refresh-token familyProduction IRefreshTokenStore in DI and reuse test passes
Real notificationProbe message, provider receipt, failure alert
FederationProfile/Adapter status, redacted configuration, connection test, callback, and JIT/binding evidence
Multi-instance MFADistributed challenge cache and cross-instance test
Permission catalogGovernance output and authorization contract tests

Identity overview · Login security · Testing and operations

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%