Skip to content

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: UserIdentity and TenantIdentity represent authenticated users and their tenant context
  • Access Control: AccessChecker enforces 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:

python
# 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:

python
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 via KeycloakAdminService.tenant_exists() before trusting metadata.
  • Contains access_rules that 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_id is 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 UserTenantRoleEntity per tenant

Accessing User Roles ​

python
# 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:

python
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_DENIED

Two-Stage Access Checking ​

CRITICAL: Tenant access rules act as a CEILING/BOUNDARY for user permissions.

  1. STAGE 1: Determine tenant's access level (admin or user)
  2. STAGE 2: Determine user's access level (admin or user)
  3. STAGE 3: Return MINIMUM of both levels

Example:

python
# 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_USER

Access 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:

python
"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 agent

Convenience Methods ​

python
# 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 ​

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

python
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:

python
# ❌ 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 context

2. Verify Tenant Membership ​

Always verify users have access to the tenant before performing operations:

python
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:

python
# ❌ 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:

python
# 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 resources

Troubleshooting ​

"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:

python
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:

python
tenant = TenantMetadataEntity.get_metadata_by_tenant_id(tenant_id)
print(tenant.access_rules)  # Check what the tenant allows

Empty 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:

python
tenant.access_rules = ["aihub.user.>"]
tenant.save()

Security Considerations ​

  • Never mount TestAuthHandler on production entry points — it lives under core.testing for this reason; production app/main.py files must use KeycloakAuthHandler or TokenAuthHandler
  • 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.

Built with ❤️ in Switzerland 🇨🇭