cratis-arc-identity-and-tenancy-kotlin · git:20261012.0af903a · 2026-10-12 · sha256 790cb50e2956a892
cratis-arc-identity-and-tenancy-kotlin git:20261012.0af903aA
Immutable. This exact content never changes and is served at /api/v1/blob/790cb50e2956a892.
--- name: cratis-arc-identity-and-tenancy-kotlin description: Wire Kotlin or Java Arc authentication, application identity details and tenant resolution — AuthenticationHandler, IdentityDetailsProvider, tenancy.required, resolver precedence, ambient coroutine tenancy and the opt-in trusted platform-header bridge. Use for JVM Arc identity endpoints, tenant isolation or security ingress setup. Do not use for C# Arc identity, command input validation, or standalone Chronicle namespace resolution. license: MIT --- <!-- Copyright (c) Cratis. All rights reserved. --> <!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. --> # Resolve identity and tenancy in JVM Arc Authentication identifies the caller; authorization decides whether they may execute an operation; tenant resolution selects their data boundary. Configure and verify all three separately. A tenant header alone proves no membership. ## Verified product sources Verified at `Arc.Kotlin v7.7.0`: `Source` authentication/identity/tenancy contracts, `Integrations/SpringBoot/ArcTenancyProperties.java`, platform security classes, `Documentation/guides/security.mdx`, `ambient-tenancy.mdx` and `reference/configuration.mdx`. Use `io.cratis:arc-spring-boot-starter:7.7.0` (or `io.cratis:cratis:7.7.0` for Chronicle integration), with matching plugin/KSP. These are JVM contracts; do not infer the same switches exist in Arc .NET. ## Authenticate and expose identity details Register ordered `AuthenticationHandler` beans. A handler gets immutable `AuthenticationRequestContext`: case-insensitive headers, cookies, captured principal and selected tenant. Return: - `AuthenticationResult.ANONYMOUS` when credentials are not recognized, so the next handler can try. - `AuthenticationResult.failed(...)` for recognized but rejected credentials. - `AuthenticationResult.succeeded(...)` with an `ArcPrincipal` on success. Success and failure are terminal; a later handler cannot override a rejection. Spring interleaves Kotlin handlers and Java `AsyncAuthenticationHandler` beans in global `@Order`. Java handlers return `CompletionStage`, which Arc adapts. Do not block while verifying credentials. Public authentication errors are generic 401 responses, not the handler's private reason. With Spring Security present, Arc captures its principal before suspending work. `ROLE_` authorities become unprefixed Arc roles. Without it, Arc captures the servlet principal. Never read thread-local security state after suspension. Register exactly one `IdentityDetailsProvider<T>` to serve `GET /.cratis/me`. Supply `detailsType: Class<T>` and return `IdentityDetails<T>` from `provide(IdentityProviderContext)`, including `isUserAuthorized` and the application payload. Use a concrete DTO KSP can describe; an unsupported identity shape fails `ARCKSP0307`. Java uses `AsyncIdentityDetailsProvider<T>`. Multiple providers fail startup. The client-readable identity cache cookie is excluded from authentication input and cannot authenticate a later request. `@Authorize`, `@Roles`, `@AllowAnonymous` declare authorization. An operation's own metadata replaces its class default wholesale; contradictions on the same declaration fail `ARCKSP0108`. See `cratis-arc-command-kotlin` for precedence. ## Configure inbound tenant resolution Keys below are under `cratis.arc`: | Key | Default / contract | | --- | --- | | `tenancy.resolvers` | `header`; ordered `fixed`, `header`, `query`, `claim`, `subdomain`, `development` | | `tenancy.required` | `false`; true rejects unresolved command, query, observable and identity requests with 400 | | `tenancy.header-name` | unset uses `tenant-header` (default `x-cratis-tenant-id`); case-insensitive | | `tenancy.query-parameter-name` | `tenantId`; case-sensitive | | `tenancy.claim-type` | `tenant_id` | | `tenancy.base-domain` | empty; subdomain resolution needs a valid multi-label DNS base domain | | `tenancy.fixed-tenant-id` | `development` | | `tenancy.constrain-to-authenticated-claims` | `true`; when membership claims exist, selection must match one | Keep claim constraints on. Absence of membership claims is not proof the caller belongs to every tenant; add the application authorization policy required by your domain. Do not expose development/fixed selection as production isolation. `TenantsProvider` lists development tenants for tools; it does not resolve a request. Resolution is not storage routing. Plain repository injection is ordinary Spring DI, not automatic tenant isolation. Use certified tenant-aware store resolvers and test two-tenant isolation; `cratis-arc-spring-data-kotlin` covers the storage boundary. Chronicle namespace mapping is a separate integration concern. ## Carry an already resolved tenant `withTenant(TenantId(...))` installs a `TenantCoroutineContext` that survives suspension and dispatcher switches. `currentTenant()` returns the nearest tenant or `null`; `currentTenantOrNotSet()` returns `TenantId.NOT_SET` when absent. Nested scopes shadow the outer tenant for their duration. These functions do not resolve HTTP requests or authorize a tenant. Java on a plain thread uses `TenantContextBridge.withTenant` and `TenantContextBridge.currentTenant()`. The bridge blocks through `runBlocking`; do not call it inside an active coroutine. Kotlin coroutine code uses the suspending APIs, not a `ThreadLocal`. Establish scopes explicitly for background work; do not assume an inbound request or command establishes every ambient seam. ## Opt into trusted platform headers only behind secure ingress `cratis.arc.platform-identity.enabled` defaults to `false`. Base64 is unsigned input, not authentication. Enable the bridge only behind authenticated ingress that strips caller-supplied `x-ms-client-principal`, `x-ms-client-principal-id`, `x-ms-client-principal-name` and rewrites them from verified identity. Block direct backend access. Supply an `ArcPlatformIdentityTrust` bean checking the actual deployment boundary on every submission; the default denies all. Do not trust forwarded headers or an address a request wrapper rewrote without verified proxy handling. Header parsing cannot install firewall, TLS or ingress guarantees. With Spring Security absent or the bridge disabled, Arc registers no platform filter/chain. With no application chain, enabling it creates a conservative chain: all routes need authentication and CSRF remains enabled. Arc does not rewrite an application `SecurityFilterChain`; add `ArcPlatformAuthenticationFilter` after existing authentication and before `AnonymousAuthenticationFilter` only in chains intended to accept platform identity. Keep servlet registration disabled to prevent double execution outside Spring Security. Valid submissions never replace an already authenticated non-anonymous identity. Invalid/partial/repeated submissions fail generically with 401 (earlier filters may reject first). The bridge retains identity in request attributes, not the HTTP session. Never log identity headers, raw claims or the cache cookie. Consult the tagged security guide for the bounded header schema and limits before changing an ingress adapter; do not hand-roll permissive parsing. ## Verify and route - Test the resolver order, missing tenant, membership mismatch and valid selection with `withTenantResolution` in `cratis-arc-testing-kotlin`. - Exercise anonymous, accepted and rejected credentials plus identity-details authorization; do not mistake a successful details response for authentication. - Verify tenant propagation across a dispatcher switch and two isolated stores. - If enabling platform identity, test spoofed/direct submissions fail, legitimate ingress succeeds, CSRF remains enforced, and the filter runs once. - Run the application's `./gradlew build` and real storage/ingress checks. Command rules: `cratis-arc-validation-kotlin`. Standalone Chronicle namespaces: `cratis-chronicle-client-kotlin`, not this resolver configuration.