> ## Documentation Index
> Fetch the complete documentation index at: https://docs.emergence.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Keycloak multi-realm OIDC/PKCE authentication with JWT token validation, service accounts, and SSO for CRAFT.

# Authentication

CRAFT uses **Keycloak** as its identity provider, configured in a **multi-realm architecture** where each organization is a separate Keycloak realm. Authentication follows the **OpenID Connect (OIDC)** protocol with **PKCE** (Proof Key for Code Exchange) for browser-based clients.

<Info>
  Keycloak is self-hosted -- the platform's identity layer runs on your infrastructure and does not depend on a cloud-native identity service. You can federate external enterprise identity providers (Entra ID, Okta, Google Workspace) through standard OIDC and SAML protocols.
</Info>

## Multi-Realm Architecture

Each organization maps to a dedicated Keycloak realm:

```text theme={null}
Keycloak Server
  master realm          -- Platform developers and service accounts
  acme-corp realm       -- Acme Corporation users
  globex realm          -- Globex Industries users
  initech realm         -- Initech users
```

This provides complete identity isolation:

* Users in one realm cannot see or access users in another
* Each realm has independent password policies, session settings, and identity providers
* Group memberships are realm-scoped and appear as JWT claims
* SSO can be configured per-realm (e.g., SAML, LDAP, social login)

## Authentication Flow

```mermaid theme={null}
%%{init: {'theme': 'base', 'themeVariables': {
  'actorBkg': '#56B4E9',
  'actorBorder': '#555555',
  'actorTextColor': '#000000',
  'actorLineColor': '#555555',
  'signalColor': '#555555',
  'signalTextColor': '#555555',
  'labelBoxBkgColor': '#EEEEEE',
  'labelBoxBorderColor': '#555555',
  'labelTextColor': '#555555',
  'loopTextColor': '#555555',
  'noteBkgColor': '#F0E442',
  'noteBorderColor': '#555555',
  'noteTextColor': '#000000',
  'activationBkgColor': '#E69F00',
  'activationBorderColor': '#555555',
  'fontFamily': 'sans-serif'
}}}%%
sequenceDiagram
    participant B as Browser
    participant KC as Keycloak Realm
    participant API as Platform API
    B->>B: Generate PKCE code_verifier + code_challenge
    B->>KC: Authorization request + code_challenge
    KC->>B: Redirect to login page
    B->>KC: User credentials
    KC-->>B: Authorization code (redirect)
    B->>KC: Token request + code_verifier
    KC-->>B: Access token (JWT) + refresh token + ID token
    B->>API: API request + Authorization: Bearer JWT
    API->>API: Validate JWT, extract org_id from realm
    API-->>B: Response
```

<Steps>
  <Step title="OIDC Discovery">
    The client discovers the realm's OpenID configuration at:

    ```text theme={null}
    https://keycloak.example.com/realms/{org_id}/.well-known/openid-configuration
    ```
  </Step>

  <Step title="Authorization Request (PKCE)">
    The browser initiates the OIDC authorization code flow with PKCE. A code verifier is generated client-side and the SHA-256 code challenge is sent with the authorization request. No client secret is required.
  </Step>

  <Step title="User Login">
    The user authenticates at the Keycloak login page for their realm. Keycloak supports username/password, SSO via external IdPs (SAML, OIDC federation), LDAP, and social login.
  </Step>

  <Step title="Token Exchange">
    After successful login, Keycloak returns an authorization code. The client exchanges it (with the code verifier) for:

    * **Access token (JWT)** -- Used for API authorization
    * **Refresh token** -- Used to obtain new access tokens
    * **ID token** -- Contains user identity claims
  </Step>

  <Step title="API Requests">
    All API requests include the access token in the `Authorization: Bearer` header. The platform validates the token and extracts the organization context from the realm.
  </Step>
</Steps>

## JWT Token Structure

The access token contains standard OIDC claims plus platform-specific claims:

```json theme={null}
{
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "iss": "https://keycloak.example.com/realms/acme-corp",
  "aud": "account",
  "azp": "em-runtime-ui",
  "exp": 1743696000,
  "iat": 1743692400,
  "preferred_username": "jane.smith",
  "email": "jane.smith@acme-corp.com",
  "name": "Jane Smith",
  "groups": ["/org-admins", "/project-developers"],
  "realm_access": {
    "roles": ["default-roles-acme-corp"]
  }
}
```

Key claims used by the platform:

| Claim         | Purpose                                                                    |
| ------------- | -------------------------------------------------------------------------- |
| `sub`         | User ID for OpenFGA permission checks                                      |
| `iss`         | Realm extraction -- the path segment after `/realms/` becomes `org_id`     |
| `groups`      | Keycloak group memberships, used for group-based OpenFGA permission checks |
| `azp`         | Client ID of the application that requested the token                      |
| `exp` / `iat` | Token expiration and issuance timestamps                                   |

## Token Validation

The Governance service validates tokens using a multi-step process:

<Tabs>
  <Tab title="Validation Steps">
    1. **Extract realm** -- The token's `iss` claim is decoded (without verification) to determine the Keycloak realm
    2. **Fetch JWKS** -- Public keys are fetched from the realm's JWKS endpoint (cached per realm via `lru_cache`)
    3. **Verify signature** -- The token signature is validated against the realm's public key, which cryptographically proves the token was issued by the platform's Keycloak instance
    4. **Check expiration** -- Standard JWT expiration (`exp`) validation
    5. **Verify issuer consistency** -- The issuer from the verified token must match the initial unverified extraction
    6. **Extract claims** -- `org_id`, groups, roles, and user identity are extracted into the `Auth` object
  </Tab>

  <Tab title="Security Properties">
    * **Issuer hostname is not validated separately** -- JWKS signature verification is strictly stronger. A token signed by the platform's Keycloak private key is accepted regardless of the hostname in the issuer URL. This allows tokens issued via any entrypoint (internal service URL, API gateway, external hostname) to be accepted.
    * **Realm-specific validation** -- Each realm has its own signing keys. A token from `realm-a` cannot be validated against `realm-b`'s JWKS.
    * **Group normalization** -- Leading slashes are stripped from group names (`/org-admins` becomes `org-admins`).
  </Tab>
</Tabs>

## Service Accounts

Service accounts are used by background workers and automated processes that need to access the platform without user interaction. They are identified by a three-check detection. See [Service Accounts](/platform/service-accounts) for a complete guide including token management, creation steps, and code examples.

<AccordionGroup>
  <Accordion title="1. Master Realm Authentication">
    Service accounts authenticate via the Keycloak `master` realm, not an organization realm. Their `org_id` is `null` by default.
  </Accordion>

  <Accordion title="2. Client ID Convention">
    Service account client IDs must start with the `svc-` prefix (e.g., `svc-data-readiness`, `svc-talk2data`). This is a naming convention enforced by the detection logic.
  </Accordion>

  <Accordion title="3. Role Membership">
    Service accounts must have the `serviceAccount` role in `realm_access.roles`. This provides defense in depth -- even if someone creates a client with a `svc-` prefix, it will not be treated as a service account without the role.
  </Accordion>
</AccordionGroup>

Service accounts use additional headers to establish context:

| Header           | Purpose                                                                    | Required |
| ---------------- | -------------------------------------------------------------------------- | -------- |
| `X-Org-Id`       | Specifies the organization context (since master realm tokens have no org) | Yes      |
| `X-On-Behalf-Of` | Identifies the original user in audit logs when acting on their behalf     | Optional |

<Warning>
  The `X-On-Behalf-Of` and `X-Org-Id` headers are only trusted when the caller passes all three service account checks. Regular user tokens cannot use these headers to impersonate other users or switch organizations.
</Warning>

## SSO Integration

Each Keycloak realm can be configured with external identity providers for single sign-on:

| Provider Type       | Configuration                                             |
| ------------------- | --------------------------------------------------------- |
| **SAML 2.0**        | Enterprise IdP integration (Okta, Azure AD, PingFederate) |
| **OIDC Federation** | Connect to another OIDC provider                          |
| **LDAP/AD**         | Sync users from Active Directory or LDAP                  |
| **Social Login**    | Google, GitHub, Microsoft (per-realm configuration)       |

Since each organization is a separate realm, SSO configuration is fully tenant-isolated. One organization can use SAML with Okta while another uses LDAP with Active Directory.

## Obtaining Tokens

<Tabs>
  <Tab title="Development">
    ```bash theme={null}
    # Get a JWT token for the default admin user
    make get-token

    # Customize organization or credentials
    ORGANIZATION_ID=acme-corp \
      ADMIN_USERNAME=jane \
      ADMIN_PASSWORD=secret \
      make get-token
    ```
  </Tab>

  <Tab title="Platform Admin">
    ```bash theme={null}
    # Get a Keycloak platform admin token (master realm)
    make get-platform-token
    ```
  </Tab>

  <Tab title="Programmatic (OIDC)">
    ```python theme={null}
    from keycloak import KeycloakOpenID

    keycloak = KeycloakOpenID(
        server_url="https://keycloak.example.com/",
        client_id="my-app",
        realm_name="acme-corp",
    )

    # Resource Owner Password Grant (dev/testing only)
    token = keycloak.token("username", "password")
    access_token = token["access_token"]
    ```
  </Tab>
</Tabs>

## Next Steps

<CardGroup cols={2}>
  <Card title="Authorization" icon="shield-halved" href="/platform/authorization">
    Learn how JWT claims are used for OpenFGA permission checks.
  </Card>

  <Card title="Organizations" icon="building" href="/platform/organizations">
    Understand how organizations map to Keycloak realms.
  </Card>

  <Card title="Multi-Tenancy" icon="users" href="/platform/multi-tenancy">
    See the full tenant isolation architecture.
  </Card>

  <Card title="Projects" icon="folder" href="/platform/projects">
    Learn how the X-Project-ID header provides project context.
  </Card>
</CardGroup>
