Authentication Setup
Swiss AI Hub uses a multi-tenant authentication and authorization system with local role management.
Overview
The authentication system consists of several key components:
- Auth Handlers: Validate credentials and resolve user identity
- Identity Models:
UserIdentityandTenantIdentityrepresent authenticated users and their tenant context - Access Control:
AccessCheckerenforces permissions based on hierarchical access rules - Multi-Tenancy: All operations occur within a tenant context
Authentication Flow
1. Token Validation
Auth handlers validate incoming requests and extract user information:
# Keycloak OIDC JWT validation
user_identity = await KeycloakAuthHandler()(request)
# Token-based authentication
user_identity = await TokenAuthHandler()(request)Supported authentication methods:
- OAuth2/OIDC: JWT tokens from Keycloak (supports federated identity providers like Azure AD, Google, etc.)
- API Tokens: Long-lived tokens for programmatic access
- OpenWebUI Integration: Special handler for OpenWebUI users
For tests and interactive playground servers, a dedicated TestAuthHandler lives under swiss_ai_hub.core.testing.auth_utils (not core.auth) and bypasses token parsing to return a fixed test identity. It is deliberately not reachable from production code via the public auth interface.
2. User Resolution
User profile data (name, email) is read from JWT claims for OAuth2 flows or fetched via KeycloakAdminService for bearer tokens. There is no local user record — Keycloak is the single source of truth for user identity.
First User Behavior: The first user to join a tenant automatically receives admin roles. Subsequent users receive standard user roles (configurable via UserSignupSettings). This applies per-tenant, not globally, and is enforced in UserTenantRoleEntity when a new membership is created.
3. Tenant Context Resolution
Most authenticated requests have a tenant context. The tenant is identified via a {tenant_id} path parameter in the URL — most API routes are mounted at /api/v1/{tenant_id}/.... Sysadmin-only endpoints (e.g. the tenant administration controller) are mounted globally without a tenant prefix.
Tenant resolution is handled inside AuthHandler.build_identity() and AuthHandler._resolve_tenant_by_id(). The latter consults KeycloakAdminService.tenant_exists() first — Keycloak is the authority on whether a tenant exists — and only then checks UserTenantRoleEntity membership. The membership check is skipped entirely for sysadmins (see "Sysadmin access" below). Controllers do not call these resolvers directly; they are wired into user_with_permission() and sys_admin_user().
Tenant Path Parameter: All API requests must include the {tenant_id} in the URL path. Two formats are supported:
- Concrete ID:
/api/v1/507f1f77bcf86cd799439011/agents/...— directly specifies the tenant by MongoDB ObjectId - Active slug:
/api/v1/active/agents/...— resolves to the user's persisted active tenant
The active tenant is never automatically updated during request resolution. It can only be changed via a dedicated API endpoint. Health endpoints remain outside tenant scope at /api/v1/health/.
4. UserIdentity Construction
Auth handlers return a UserIdentity that includes both user and tenant information:
return UserIdentity(
id=user.id,
name=user.name,
email=user.email,
roles=roles,
acting_within_tenant=tenant, # may be None for sysadmin-only requests
is_sys_admin=is_sys_admin, # derived from the AIHubSysAdmin Keycloak realm role
)The is_sys_admin flag is the single signal for platform-admin status — it short-circuits the access checker (see "Sysadmin access" below) and is the basis for the Controller.sys_admin_user() dependency that gates sysadmin-only endpoints.
Multi-Tenant Role Management
Core Entities
TenantMetadataEntity
- Holds display metadata (name, description, access rules) for a tenant
- NOT the source of truth for tenant existence — the Keycloak group
/tenants/<id>is authoritative. Service code must verify existence viaKeycloakAdminService.tenant_exists()before trusting metadata. - Contains
access_rulesthat cap what ANY user in the tenant can access - Example:
["aihub.user.agent.>"]grants user-level access to all agents
UserTenantRoleEntity
- Maps users to tenants with specific roles
- Authoritative source for user-tenant-role relationships
- Users can have different roles in different tenants
RoleEntity
- Every role belongs to exactly one tenant —
tenant_idis required - The default role set (
AIHubUser,AIHubAdmin,AIHubAgentUser, etc.) is seeded per tenant at creation time - System-wide roles no longer exist; see ADR
2026_04_14_tenant_scoped_roles.md
User profile data
- Stored in Keycloak, not locally —
KeycloakAdminService.get_user_by_id()/find_user_by_email()for lookup - Name, email, and identity attributes all flow from Keycloak; the platform writes nothing to user records
- Roles are NOT attached to the user record — they are fetched from
UserTenantRoleEntityper tenant
Accessing User Roles
# Get user's roles in a specific tenant
roles = user.get_roles(tenant_id)
# Get all access rules for a user in a tenant
access_rules = RoleEntity.get_access_rules_for_roles(roles, tenant_id=tenant_id)Access Control
AccessChecker
The AccessChecker class performs authorization checks with two-stage access control:
from aihub_lib.auth.access.AccessChecker import AccessChecker
# Create checker from UserIdentity (includes tenant context)
checker = AccessChecker.from_user(user)
# Check access level
level = checker.access_level("aihub.user.agent.class-a.id-123")
# Returns: AccessLevel.ACCESS_ADMIN | ACCESS_USER | ACCESS_DENIEDTwo-Stage Access Checking
CRITICAL: Tenant access rules act as a CEILING/BOUNDARY for user permissions.
- STAGE 1: Determine tenant's access level (admin or user)
- STAGE 2: Determine user's access level (admin or user)
- STAGE 3: Return MINIMUM of both levels
Example:
# Tenant has: aihub.user.agent.> (user-level access to all agents)
# User has: aihub.admin.agent.> (admin-level access to all agents)
# User gets ACCESS_USER (capped by tenant boundary)
checker.access_level("aihub.user.agent.class-a.id-1") # → ACCESS_USERAccess Rule Format
Access rules follow a hierarchical dot-notation:
aihub.[admin|user].<resource>.<subresource>.<id>Wildcards:
*- Single-level wildcard:aihub.user.agent.*matches any single agent>- Multi-level wildcard:aihub.user.agent.>matches all agents and sub-resources
Examples:
"aihub.admin.>" # Full admin access to everything
"aihub.user.>" # Full user access to everything
"aihub.user.agent.>" # User access to all agents
"aihub.user.agent.class-a.*" # User access to all class-a agents
"aihub.user.agent.class-a.id-123" # User access to specific agentConvenience Methods
# Check specific agent access
has_access = checker.has_access_to_agent("class-a", "id-123")
access_level = checker.access_level_for_agent("class-a", "id-123")
# Check agent class access
has_access = checker.has_access_to_agent_class("class-a")
# Check process access
has_access = checker.has_access_to_process("workflow", "proc-456")
# Check service access
has_access = checker.has_access_to_service("llm-gateway")Configuration
Environment Variables
# Startup Tenant Configuration (seeded on first boot; an ordinary tenant thereafter)
AIHUB_STARTUP_TENANT_NAME="Swiss AI Hub"
AIHUB_STARTUP_TENANT_DESCRIPTION="This tenant was auto-created on startup of the Swiss AI Hub."
AIHUB_STARTUP_TENANT_ACCESS_RULES="aihub.admin.>"
# User Signup Role Assignment
AIHUB_USER_SIGNUP_DEFAULT_ROLES="AIHubUser"
AIHUB_USER_SIGNUP_REGULAR_USER_ROLES="AIHubUser"
AIHUB_USER_SIGNUP_FIRST_ADMIN_USER_ROLES="AIHubAdmin,AIHubUser"
# OAuth2 Configuration
OAUTH2_ENABLED=true
OAUTH2_JWKS_URL="https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys"
OAUTH2_ISSUER="https://login.microsoftonline.com/{tenant}/v2.0"
OAUTH2_AUDIENCE="api://{app-id}"Settings Classes
from swiss_ai_hub.core.infrastructure.api.startup_tenant_settings import StartupTenantSettings
from swiss_ai_hub.core.infrastructure.api.user_signup_settings import UserSignupSettings
# Access startup tenant settings
tenant_settings = StartupTenantSettings()
print(tenant_settings.access_rules_list) # ['aihub.admin.>']
# Access user signup settings
signup_settings = UserSignupSettings()
print(signup_settings.first_admin_user_roles_list) # ['AIHubAdmin', 'AIHubUser']Best Practices
1. Always Provide Tenant Context
Never create AccessChecker without tenant access rules:
# ❌ BAD: Manual construction without tenant context
checker = AccessChecker(user_access_rules, [])
# ✅ GOOD: Use factory method that extracts both user and tenant rules
checker = AccessChecker.from_user(user) # user is UserIdentity with tenant context2. Verify Tenant Membership
Always verify users have access to the tenant before performing operations:
roles = UserTenantRoleEntity.get_roles_for_user_in_tenant(user_id, tenant_id)
if not roles:
raise HTTPException(403, "User not assigned to tenant")3. Use the Minimum Required Access Level
Grant users the minimum access level needed for their role:
# ❌ BAD: Over-permissive
access_rules = ["aihub.admin.>"]
# ✅ GOOD: Scoped to specific resources
access_rules = ["aihub.user.agent.class-a.>"]4. Tenant Access Rules as Boundaries
Use tenant access rules to limit the scope of ALL users in a tenant:
# Tenant for analytics team - only access to specific agent classes
tenant_access_rules = [
"aihub.user.agent.analytics.*",
"aihub.user.service.data-pipeline"
]
# Even admin users in this tenant cannot access other resourcesTroubleshooting
"User not assigned to tenant" Error
Cause: User exists but doesn't have any roles in the requested tenant.
Solution: Assign user to tenant with roles:
UserTenantRoleEntity.create_or_update(
user_id=user_id,
tenant_id=tenant_id,
roles=["AIHubUser"]
)"Access Denied" Despite User Having Admin Roles
Cause: Tenant access rules are limiting user permissions. (Sysadmins — users with the AIHubSysAdmin Keycloak realm role — bypass this check entirely; if the issue persists for a sysadmin, the bypass itself is misconfigured.)
Solution: Check tenant access rules:
tenant = TenantMetadataEntity.get_metadata_by_tenant_id(tenant_id)
print(tenant.access_rules) # Check what the tenant allowsEmpty Tenant Access Rules = No Access
If a tenant has no access rules ([]), ALL users in that tenant are denied access to everything.
Solution: Set appropriate tenant access rules:
tenant.access_rules = ["aihub.user.>"]
tenant.save()Security Considerations
- Never mount
TestAuthHandleron production entry points — it lives undercore.testingfor this reason; productionapp/main.pyfiles must useKeycloakAuthHandlerorTokenAuthHandler - Validate JWTs properly - always verify issuer, audience, and signature
- Use HTTPS - never transmit tokens over unencrypted connections
- Rotate API tokens regularly - implement token expiration and rotation
- Audit access control changes - log all role and permission modifications
- Principle of least privilege - grant minimum required access
- Tenant isolation - users cannot access resources outside their tenant's boundaries
Migration from Previous System
Previous versions fetched roles from Azure AD via Microsoft Graph API. The new system:
- ✅ Stores roles locally in
UserTenantRoleEntity - ✅ No external API calls during authentication
- ✅ Tenant-scoped roles for multi-tenancy
- ❌ No automatic role sync from identity provider
- ❌ No automatic profile image fetching from identity provider
See ADR: Local Multi-Tenant Role Management for details.
