Final StretchOrdered learning track

Multi-Tenant Authentication

Learn Java Authentication Pattern - Part 034

Multi-tenant authentication untuk Java systems: tenant resolution, issuer/realm strategy, tenant-aware login, token validation, session binding, Spring Security AuthenticationManagerResolver, Keycloak realm/organization trade-off, data isolation, observability, dan failure modes.

11 min read2025 words
PrevNext
Lesson 3440 lesson track34–40 Final Stretch
#java#authentication#multi-tenancy#tenant-isolation+11 more

Part 034 — Multi-Tenant Authentication

Target part ini: membangun mental model dan implementation pattern untuk autentikasi multi-tenant: bagaimana tenant ditemukan, bagaimana token divalidasi per tenant, bagaimana user/account/membership dimodelkan, bagaimana session/token dibatasi tenant, dan bagaimana mencegah confused deputy serta cross-tenant login bug.

Multi-tenant authentication adalah salah satu tempat paling berbahaya untuk “asumsi kecil”.

Contoh asumsi kecil:

User email is globally unique.
Tenant can be inferred from subdomain.
All tenants use the same IdP.
Token issuer is enough.
One session can switch tenant safely.
Admin in tenant A is admin everywhere.

Asumsi seperti ini sering tidak meledak saat MVP. Ia meledak saat enterprise tenant, federation, SSO, custom domain, delegated admin, merger organisasi, atau regulatory audit masuk.

Autentikasi multi-tenant harus menjawab empat pertanyaan sebelum user dianggap authenticated untuk tenant tertentu:

Which tenant is this authentication attempt for?
Which identity provider is authoritative for that tenant?
Which account/membership does the external identity map to?
Which tenant boundary is bound to the resulting session/token?

1. Tenant Is a Security Boundary, Not a UI Filter

Tenant bukan sekadar kolom tenant_id di database.

Dalam authentication, tenant menentukan:

AreaDampak tenant
Login routingIdP mana, realm mana, policy mana
Credential storepassword/passkey milik tenant mana
Session bindingsession valid untuk tenant apa
Token validationissuer/JWKS/audience mana
MFA policyfactor dan step-up per tenant
Recovery flowsiapa boleh recover account
Audittenant context semua auth event
Rate limitingper tenant + global abuse control
Authorization handoffmembership/role/scope tenant mana

Invariant utama:

No authenticated principal exists without an explicit tenant context
unless the operation is intentionally tenant-discovery or global-admin scoped.

2. Identity Model: Person, Account, Membership

Jangan langsung samakan User dengan tenant user.

Production-grade model biasanya memisahkan:

Satu manusia bisa punya:

account berbeda di tenant berbeda
external identity berbeda per enterprise tenant
email sama tapi issuer berbeda
membership aktif di tenant A, suspended di tenant B
MFA requirement berbeda per tenant

Do not use email as global identity key.

Safer federated identity key:

(issuer, subject)

Safer tenant membership key:

(tenant_id, account_id)

3. Tenant Resolution Strategies

Tenant harus ditemukan sebelum authentication dapat diarahkan.

StrategyExampleStrengthRisk
Subdomaintenant-a.app.comNatural SaaS routingcustom domain complexity
Custom domainlogin.customer.comEnterprise-friendlydomain ownership proof needed
Path/t/tenant-a/loginSimpleleak tenant in URL, routing mistakes
HeaderX-Tenant-IdAPI-friendlyspoofable unless boundary controlled
Login identifieruser enters email firstGood for IdP discoveryenumeration risk
Token issueriss=https://idp/realms/aStrong post-authnot available pre-login
Organization claimorg_id=tenant-aGood if trusted IdPmust validate issuer/audience

Use different resolution modes for different phases.

Pre-auth browser login:
  subdomain/custom domain/identity-first discovery

API request with token:
  issuer + audience + tenant claim validation

Internal service call:
  mTLS/service identity + explicit tenant context

Admin/global operation:
  explicit global scope + audited tenant selection

4. Tenant Resolution Flow

Critical checks:

state must bind tenant id
redirect_uri must match tenant/client registration
issuer must match tenant policy
subject must map to account
membership must be active for tenant
session must be bound to tenant

5. The Login Boundary: Unauthenticated but Tenant-Aware

Before authentication, user is not known.

But tenant may already be known. This is a special state:

TenantResolvedUnauthenticated

Do not treat it as authenticated.

This model prevents a subtle bug:

External identity is valid, but not valid for this tenant.

6. Tenant Binding in Sessions

A session must carry tenant context.

public record TenantAuthenticatedPrincipal(
        String accountId,
        String tenantId,
        String subject,
        String issuer,
        String sessionId,
        String assuranceLevel,
        Set<String> roles,
        Instant authenticatedAt
) implements Serializable {}

Session invariant:

Every authenticated session is bound to exactly one active tenant context,
or is a clearly marked global-admin session with explicit tenant switching controls.

Tenant switching

If user belongs to multiple tenants, there are two safer models.

Model A: One session per tenant

login tenant-a -> session tenant-a
switch tenant-b -> new auth/session or verified tenant switch event

Pros: clear audit, clear cookie/session semantics, simple authorization checks. Cons: more redirects and more session entries.

Model B: One global login + active tenant context

login once -> account session
select active tenant -> tenant-bound context inside session

Pros: better UX. Cons: higher risk of confused tenant context; every action must bind CSRF/state/request to active tenant.

For regulated workflows, prefer explicit tenant-bound session for sensitive operations.


Cookie config can break tenant isolation.

Dangerous cookie:

Set-Cookie: SESSION=abc; Domain=.app.com; Path=/; Secure; HttpOnly; SameSite=Lax

This cookie is shared across subdomains.

If each tenant is on subdomain:

tenant-a.app.com
tenant-b.app.com

A broad cookie domain can cause session confusion if application routing is weak.

ModelCookie domainNotes
Tenant-specific subdomain sessionhost-only cookieStrong isolation
Central login domainlogin cookie on login domain onlyApp receives tenant session after callback
BFF same originhost-only session cookieRecommended for browser apps
Cross-tenant dashboardstrict active tenant bindingMore complex

Rule:

Cookie scope must not be broader than the trust boundary you can enforce.

8. JWT Claims for Multi-Tenant Authentication

A tenant-aware access token should make boundary explicit.

{
  "iss": "https://idp.example.com/realms/tenant-a",
  "sub": "f7b4a9d2-...",
  "aud": "case-api",
  "azp": "web-bff",
  "exp": 1783070400,
  "iat": 1783066800,
  "tenant_id": "tenant-a",
  "org_id": "tenant-a",
  "membership_id": "mbr_123",
  "membership_version": 42,
  "acr": "aal2",
  "amr": ["pwd", "otp"],
  "scope": "case:read case:write"
}
ClaimPurpose
issIdP/realm authority
substable subject under issuer
audresource server boundary
azp / client_idclient/application identity
tenant_id / org_idtenant boundary
membership_idaccount-tenant relationship
membership_versiondetect stale role/membership
acrauthentication assurance
amrauthentication methods

Never trust tenant_id alone without validating issuer and audience.

tenant_id is a claim.
issuer + signature + audience + trust config make it meaningful.

9. Issuer Strategies

Strategy A: Realm/issuer per tenant

https://idp.example.com/realms/tenant-a
https://idp.example.com/realms/tenant-b

Pros:

strong isolation
separate keys/policies/clients
enterprise customization
simple tenant inference from iss

Cons:

operational overhead
many realms/clients
cross-tenant user journey harder
key rotation/config scaling

Strategy B: Single issuer with tenant/org claim

iss=https://idp.example.com/realms/saas
org_id=tenant-a

Pros:

simpler operations
shared login UX
shared clients
better for many small tenants

Cons:

tenant isolation relies on claims/policy
misconfiguration can leak roles
custom enterprise federation harder

Strategy C: Hybrid

small tenants -> shared realm/org model
enterprise tenants -> dedicated realm/issuer
regulated tenants -> dedicated IdP or brokered federation

Most real platforms end up hybrid.


10. Keycloak Multi-Tenant Patterns

Keycloak can be used in several tenant models.

PatternHow it worksFit
Realm per tenanteach tenant has realmstrong isolation, enterprise
Single realm + groups/rolestenants represented inside one realmsmall/medium SaaS
Single realm + organizationsorganization-aware CIAM styleSaaS tenant membership
Identity brokeringtenant IdP federated into platformenterprise SSO
Dedicated Keycloak instancetenant has instancehigh isolation/regulatory

Keycloak Organizations feature introduces organization-aware authentication behavior in a realm. That makes it relevant for SaaS/CIAM scenarios where a user authenticates in the context of an organization. But it does not eliminate application-side tenant authorization and data isolation.

Realm per tenant mental model

Single realm organization model

Decision rule:

If tenant-specific identity policy, federation, keys, compliance, or blast-radius isolation matters strongly, prefer dedicated issuer/realm.
If operational scale and uniform policy matter more, use shared issuer with strong organization/tenant claim controls.

11. Spring Security: Tenant-Aware JWT Validation

For resource servers, Spring Security can route authentication by request using AuthenticationManagerResolver<HttpServletRequest>. This is the right extension point when issuer/tenant varies per request.

public interface TenantResolver {
    Optional<TenantAuthConfig> resolve(HttpServletRequest request);
}

public record TenantAuthConfig(
        String tenantId,
        String issuer,
        String jwkSetUri,
        Set<String> audiences
) {}
@Bean
AuthenticationManagerResolver<HttpServletRequest> tenantAuthenticationManagerResolver(
        TenantResolver tenantResolver,
        JwtDecoderFactory<TenantAuthConfig> decoderFactory
) {
    ConcurrentMap<String, AuthenticationManager> managers = new ConcurrentHashMap<>();

    return request -> {
        TenantAuthConfig tenant = tenantResolver.resolve(request)
                .orElseThrow(() -> new BadCredentialsException("Unknown tenant"));

        return managers.computeIfAbsent(tenant.tenantId(), key -> {
            JwtAuthenticationProvider provider = new JwtAuthenticationProvider(decoderFactory.createDecoder(tenant));
            provider.setJwtAuthenticationConverter(tenantAwareJwtAuthenticationConverter(tenant));
            return provider::authenticate;
        });
    };
}
@Bean
SecurityFilterChain apiSecurity(
        HttpSecurity http,
        AuthenticationManagerResolver<HttpServletRequest> resolver
) throws Exception {
    return http
            .csrf(csrf -> csrf.disable())
            .oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(resolver))
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/actuator/health").permitAll()
                    .anyRequest().authenticated()
            )
            .build();
}

Tenant-aware decoder:

public final class TenantJwtDecoderFactory implements JwtDecoderFactory<TenantAuthConfig> {

    @Override
    public JwtDecoder createDecoder(TenantAuthConfig tenant) {
        NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(tenant.jwkSetUri()).build();

        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(tenant.issuer());
        OAuth2TokenValidator<Jwt> withAudience = jwt -> {
            boolean ok = jwt.getAudience().stream().anyMatch(tenant.audiences()::contains);
            return ok
                    ? OAuth2TokenValidatorResult.success()
                    : OAuth2TokenValidatorResult.failure(new OAuth2Error("invalid_token", "Invalid audience", null));
        };
        OAuth2TokenValidator<Jwt> withTenant = jwt -> {
            String tenantId = jwt.getClaimAsString("tenant_id");
            return tenant.tenantId().equals(tenantId)
                    ? OAuth2TokenValidatorResult.success()
                    : OAuth2TokenValidatorResult.failure(new OAuth2Error("invalid_token", "Invalid tenant", null));
        };

        decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(withIssuer, withAudience, withTenant));
        return decoder;
    }
}

Critical invariant:

Tenant resolution and token validation must agree.

If host says tenant A but token says tenant B, reject.


12. Tenant Context Filter

Even after authentication, application code needs tenant context. Use a scoped context with cleanup.

public final class TenantContextHolder {
    private static final ThreadLocal<String> TENANT = new ThreadLocal<>();

    public static void set(String tenantId) {
        TENANT.set(Objects.requireNonNull(tenantId));
    }

    public static String requireTenantId() {
        String tenantId = TENANT.get();
        if (tenantId == null) {
            throw new IllegalStateException("Tenant context missing");
        }
        return tenantId;
    }

    public static void clear() {
        TENANT.remove();
    }
}
public final class TenantContextFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {
        try {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication instanceof JwtAuthenticationToken jwtAuth) {
                String tenantId = jwtAuth.getToken().getClaimAsString("tenant_id");
                TenantContextHolder.set(tenantId);
            }
            filterChain.doFilter(request, response);
        } finally {
            TenantContextHolder.clear();
        }
    }
}

Important:

ThreadLocal tenant context must be cleared exactly like SecurityContext.

Otherwise tenant leakage can happen across pooled servlet threads.


13. Tenant-Aware OIDC Login

Resource server validation handles API tokens. Interactive login needs dynamic OIDC client registration.

You need to bind:

login request tenant -> client registration -> state -> callback -> session tenant

State must bind tenant.

Bad:

state=randomOnly

Better:

state = signed(random + tenantId + redirectIntent + issuedAt)

On callback:

state tenant must equal tenant from callback route/session
issuer must equal tenant expected issuer
token nonce must match original login request

If state is not tenant-bound, login CSRF and tenant confusion become possible.

Account linking rule

Never auto-link solely by email:

email=alice@example.com from issuer A
email=alice@example.com from issuer B

These can be different identities.

Safer auto-link precondition:

same verified issuer+subject already known
or explicit admin-approved federation mapping
or user-authenticated linking ceremony with step-up

14. Tenant-Aware Password Authentication

For local password login, tenant matters before credential lookup.

create table account_login_identifier (
    tenant_id text not null,
    identifier_normalized text not null,
    account_id uuid not null,
    primary key (tenant_id, identifier_normalized)
);

create table account_password_credential (
    credential_id uuid primary key,
    tenant_id text not null,
    account_id uuid not null,
    password_hash text not null,
    password_version int not null default 1,
    status text not null,
    created_at timestamptz not null,
    updated_at timestamptz not null,
    unique (tenant_id, account_id)
);

Login flow:

resolve tenant
normalize identifier
lookup (tenant_id, identifier)
if missing -> synthetic password verification
if found -> verify credential
check membership status
check tenant policy/MFA
create tenant-bound session

Generic response remains required:

Invalid username or password.

Do not reveal:

unknown tenant
unknown email in this tenant
email exists in another tenant
membership suspended
federated-only user

Those details can be shown only after stronger verification or admin flow.


15. Rate Limiting in Multi-Tenant Authentication

Rate limiting must combine global and tenant dimensions.

Dimensions:

source_ip
tenant_id
identifier_hash
tenant_id + identifier_hash
client_id
device_fingerprint
asn/country risk bucket
AttackNeeded dimension
Credential stuffing across tenantsglobal IP/device/client
Targeted attack on one tenanttenant_id + identifier
Tenant lockout DoSper-identifier soft throttle, avoid hard lockout abuse
Enumeration via tenant discoverytenant discovery endpoint throttle

Redis key examples:

auth:rl:ip:203.0.113.10
auth:rl:tenant:tenant-a:ip:203.0.113.10
auth:rl:tenant:tenant-a:identifier:sha256(email)
auth:rl:client:web-bff

16. Multi-Tenant Authorization Handoff

Authentication should produce a tenant-bound principal. Authorization should not rediscover tenant from request again.

public record AuthenticatedTenantPrincipal(
        String accountId,
        String tenantId,
        String membershipId,
        int membershipVersion,
        Set<String> authorities,
        String issuer,
        String subject
) {}

Handoff invariant:

Authentication validates identity and tenant membership.
Authorization evaluates permissions only within that authenticated tenant boundary.

Do not let controller accept tenant from user input and override authenticated tenant.

Bad:

@GetMapping("/tenants/{tenantId}/cases/{caseId}")
public CaseDto get(@PathVariable String tenantId, @PathVariable String caseId) {
    return caseService.get(tenantId, caseId);
}

Better:

@GetMapping("/cases/{caseId}")
public CaseDto get(@AuthenticationPrincipal AuthenticatedTenantPrincipal principal,
                   @PathVariable String caseId) {
    return caseService.get(principal.tenantId(), caseId);
}

If tenant appears in path for routing/UX, verify it equals authenticated tenant.

if (!pathTenantId.equals(principal.tenantId())) {
    throw new AccessDeniedException("Tenant mismatch");
}

17. Data Layer Guardrails

Authentication mistakes should not be the only line of defense.

Every tenant-owned table needs tenant key.

create table regulatory_case (
    tenant_id text not null,
    case_id uuid not null,
    title text not null,
    status text not null,
    created_by_account_id uuid not null,
    created_at timestamptz not null,
    primary key (tenant_id, case_id)
);

Repository methods should require tenant id.

Optional<CaseEntity> findByTenantIdAndCaseId(String tenantId, UUID caseId);

Dangerous method:

Optional<CaseEntity> findByCaseId(UUID caseId);

It encourages cross-tenant bugs.

For high assurance, consider database-level tenant guardrails such as PostgreSQL row-level security. But do not use RLS as an excuse for sloppy application auth. Use it as defense-in-depth.


18. Multi-Tenant MFA Policy

MFA policy often varies by tenant.

TenantPolicy
small tenantpassword + optional TOTP
enterprise tenantSSO required + phishing-resistant MFA
regulator tenantpasskey required for decision approval
partner tenantmTLS + client credentials for API access

Authentication result should record assurance.

{
  "tenant_id": "tenant-a",
  "acr": "aal2",
  "amr": ["federated", "webauthn"]
}

Sensitive operation can require step-up:

if (!principal.assuranceLevel().atLeast("aal2")) {
    throw new StepUpRequiredException("AAL2 required for enforcement decision");
}

Tenant policy must be evaluated after tenant is resolved but before session/token is considered sufficient.


19. Confused Deputy in Multi-Tenant Auth

Classic cross-tenant confused deputy:

User is admin in tenant A.
Request includes tenantId=tenant B.
Service checks user has ADMIN somewhere.
Service mutates tenant B.

Correct check:

User has ADMIN membership in tenant B.

Bad authority:

ROLE_ADMIN

Better authority:

TENANT_ADMIN scoped to tenant_id=tenant-b

In Spring, avoid unscoped role checks for tenant-owned operations.

Bad:

@PreAuthorize("hasRole('ADMIN')")

Better:

@PreAuthorize("@tenantAuthz.canManageCase(authentication, #caseId)")

Where tenantAuthz loads aggregate tenant and compares against authenticated tenant membership.


20. Cross-Tenant Token Substitution

Attack:

Request host: tenant-a.app.com
Authorization token: valid token for tenant-b

If API only validates token signature, it may accept tenant B token on tenant A route.

Control:

resolvedTenantFromRequest == token.tenant_id == expected issuer mapping
public void assertTenantConsistency(HttpServletRequest request, Jwt jwt) {
    String routeTenant = tenantResolver.resolveFromRequest(request)
            .orElseThrow(() -> new BadCredentialsException("Unknown tenant"));

    String tokenTenant = jwt.getClaimAsString("tenant_id");

    if (!routeTenant.equals(tokenTenant)) {
        throw new BadCredentialsException("Tenant mismatch");
    }
}

For APIs without tenant in host/path, token tenant can be the source of tenant context, but only after issuer/audience validation.


21. Tenant Discovery Without Enumeration

Identity-first login often asks for email first:

Enter your email to continue

Then system routes to correct tenant/IdP. Risk: attacker can enumerate tenant membership.

Bad response:

No enterprise SSO configured for alice@example.com

Better response:

If this account can sign in, we will continue to the appropriate sign-in method.

But UX often needs routing. Use layered mitigation:

generic response text
rate limiting per IP/email hash
delayed response normalization
tenant domain verification
optional email magic link for ambiguous routing
admin-controlled domain-to-tenant mapping
no disclosure of tenant membership before proof

For enterprise domains:

example.com -> tenant-a IdP

But domain ownership must be verified before routing all users of a domain.


22. Admin and Support Access

Support/admin access is where tenant auth often breaks.

Never model support as “super admin can become any user”.

Safer support session:

support_actor_id = employee_123
acting_tenant_id = tenant-a
impersonated_account_id = user_456 optional
reason_code = support_ticket_789
approval_id = approval_abc optional
expires_at = short TTL

Audit must record both:

{
  "actor_type": "SUPPORT_OPERATOR",
  "actor_id": "employee_123",
  "tenant_id": "tenant-a",
  "on_behalf_of_account_id": "user_456",
  "reason": "support_ticket_789"
}

Invariant:

Support access must never erase the real authenticated operator.

For regulated systems, support access should require step-up, approval, short TTL, and immutable audit.


23. Observability

Log every authentication event with tenant context.

EventRequired fields
tenant resolvedtenant_id, method, host/path/domain, correlation_id
login startedtenant_id, client_id, provider_id, correlation_id
login succeededtenant_id, account_id, issuer, subject hash, assurance
login failedtenant_id if known, reason_class, identifier_hash
token rejectedroute_tenant, token_issuer, token_tenant, reason
membership rejectedtenant_id, account_id, membership_status
tenant switchfrom_tenant, to_tenant, account_id, assurance
support accessoperator_id, tenant_id, target_account_id, reason

Avoid logging raw tokens, passwords, cookies, authorization codes, or full SAML assertions.

Metrics:

auth.tenant_resolution.failure.count{method,reason}
auth.login.success.count{tenant,provider}
auth.login.failure.count{tenant,reason_class}
auth.token_rejected.count{reason,issuer,route_tenant}
auth.cross_tenant_mismatch.count{route_tenant,token_tenant}
auth.membership_rejected.count{tenant,status}

Alert examples:

sudden spike in tenant mismatch
unknown issuer tokens appear
login failures concentrated on one tenant
same account tries many tenants
support access outside business hours

24. Testing Matrix

Tenant resolution tests:

known subdomain -> tenant resolved
unknown subdomain -> generic reject
custom domain unverified -> reject
header tenant from public internet -> reject
host tenant A + path tenant B -> reject

Token validation tests:

token issuer A on tenant A route -> accept
token issuer B on tenant A route -> reject
token valid signature but wrong audience -> reject
token valid issuer but missing tenant claim -> reject
token tenant claim mismatch -> reject
expired token -> reject
unknown kid -> reject or refresh JWKS under bounded policy

Membership tests:

valid external identity but no tenant membership -> reject
suspended membership -> reject
membership version stale -> refresh or reject
admin in tenant A accessing tenant B -> reject
user belongs to A and B but active session A accessing B route -> reject unless explicit switch

Session tests:

session tenant A used on tenant B host -> reject
tenant switch rotates session or records switch event
logout tenant A does not leave active tenant A session elsewhere unless intended
cookie domain does not leak across tenant subdomains

Federation tests:

same email from two issuers does not auto-link
issuer subject change handled through explicit migration
SAML assertion audience mismatch rejected
OIDC state tenant mismatch rejected

25. Migration Path from Single-Tenant to Multi-Tenant Auth

Do not migrate by sprinkling tenantId everywhere randomly.

Recommended sequence:

1. Introduce tenant table and explicit default tenant.
2. Add tenant_id to auth/account/session tables.
3. Change identity keys from email/global user to tenant-aware account/membership.
4. Add tenant resolver at request boundary.
5. Bind sessions/tokens to tenant.
6. Update repositories to require tenant_id.
7. Add tenant mismatch detection logs in shadow mode.
8. Enforce tenant mismatch rejection.
9. Add per-tenant IdP/federation support.
10. Add operational dashboards and runbooks.

During transition, use compatibility checks:

legacy account without tenant -> mapped to default tenant
legacy session -> upgraded to tenant-bound session at next request
legacy token -> short TTL, migration-only acceptance

Never allow legacy global tokens indefinitely.


26. Failure Modes

Failure modeCauseImpactControl
Email as global identitySame email across issuers/tenantsAccount takeover/linking bugUse (issuer, subject)
Tenant from untrusted headerPublic client sends X-Tenant-IdCross-tenant accessResolve at trusted gateway or validate strictly
Token accepted on wrong tenant hostMissing route-token tenant consistencyData breachCompare route tenant and token tenant
Shared cookie too broadDomain=.app.com without controlsSession confusionHost-only cookie / tenant-bound session
Unscoped admin roleROLE_ADMIN globalConfused deputyTenant-scoped membership check
Realm explosionrealm per tiny tenantOperational overloadHybrid strategy
Shared realm leakagebad group/role mappingCross-tenant privilegeorg/tenant claim validation + app-side checks
Auto-link by emailfederation convenienceAccount takeoverLink by issuer+subject; explicit linking ceremony
Missing tenant in auditlogs only account idnon-defensible auditlog tenant every auth event
Stale membership tokenrole changed after token issuedprivilege driftshort TTL, membership version, introspection/event revocation

27. Production Checklist

[ ] Tenant is treated as a security boundary, not only a UI filter.
[ ] Tenant resolution is explicit before login routing.
[ ] OIDC/SAML state binds tenant and redirect intent.
[ ] Token issuer, audience, tenant claim, and request tenant are validated together.
[ ] Account mapping uses issuer+subject, not email alone.
[ ] Session is bound to tenant.
[ ] Cookie domain/path matches tenant isolation model.
[ ] Local password credential lookup is tenant-aware.
[ ] Rate limiting includes tenant, identifier, IP/device, and global dimensions.
[ ] Authorization receives tenant-bound principal from authentication.
[ ] Data repositories require tenant_id for tenant-owned objects.
[ ] Admin/support access records real operator and target tenant.
[ ] Logs and metrics include tenant context without leaking tokens.
[ ] Test suite includes wrong issuer, wrong tenant, wrong audience, same email/different issuer, and unscoped admin cases.

28. Mental Model Summary

Multi-tenant authentication is the process of producing this fact:

This subject, from this trusted identity authority,
is authenticated into this tenant boundary,
with this account membership,
under this assurance level,
for this session/token/client.

It is not enough to say:

JWT valid.

A valid JWT can still be wrong for the route, tenant, audience, membership, operation, or session.

The core invariant:

Every authenticated principal must be tenant-bound before it can touch tenant-owned state.

If your implementation preserves that invariant at login, token validation, session creation, repository access, event emission, and audit logging, the system becomes much easier to defend.

If not, multi-tenancy becomes a silent privilege escalation machine.


References

Lesson Recap

You just completed lesson 34 in final stretch. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.