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

# Authentication

> How to authenticate with the Causeloop API: the WorkOS AuthKit token-exchange flow, Bearer JWTs, service accounts, MFA, SSO, and SCIM.

All Causeloop API requests are authenticated with a **Bearer JWT** in the `Authorization` header. There are two ways to obtain a token:

* **Human users** — exchange an upstream identity provider token for a Causeloop Bearer token via `POST /auth/exchange`.
* **Machine-to-machine** — create a service account and issue it a long-lived bearer token.

## The token-exchange flow (AuthBridge)

Causeloop's frontend authenticates users through [WorkOS AuthKit](https://workos.com/docs/authkit) (email/password or Google OAuth). Once WorkOS issues an access token to the browser session, your client exchanges it for a Causeloop-native token via `POST /v1/auth/exchange`. This is the token you use for all subsequent API calls.

The backend's identity provider is pluggable (`AUTH_PROVIDER=workos|auth0|okta|oidc|none`) — any configured provider verifies via JWKS the same way; WorkOS is the provider used by the current Causeloop frontend (`apps/platform`).

Exchange is **deny-by-default**: a verified token only yields a Causeloop JWT if the identity resolves to an existing membership, an accepted invitation, or (only when the workspace has explicitly opted into domain JIT) a verified-domain match. Exchange never creates a user as a side effect of sign-in on any other path — an unprovisioned identity gets a `403 forbidden` and an `auth.login.denied` audit event is written.

```mermaid theme={null}
sequenceDiagram
    participant Browser
    participant WorkOS
    participant CauseloopAPI

    Browser->>WorkOS: Sign in (email/password or Google OAuth)
    WorkOS-->>Browser: WorkOS access token (RS256)
    Browser->>CauseloopAPI: POST /v1/auth/exchange { subject_token: "<workos_access_token>" }
    CauseloopAPI->>WorkOS: Verify token via JWKS (RS256)
    CauseloopAPI-->>Browser: { access_token, refresh_token, scope, expires_in, ... }
    Browser->>CauseloopAPI: GET /v1/issues  Authorization: Bearer <access_token>
```

### Exchange endpoint

```http theme={null}
POST /v1/auth/exchange
Content-Type: application/json
```

**Request body**

```json theme={null}
{
  "subject_token": "<workos_access_token>"
}
```

**Response**

```json theme={null}
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "clp_refresh_...",
  "token_type": "bearer",
  "expires_in": 259200,
  "scope": "issues:read patterns:read ...",
  "workspace_id": "ws_01abc123"
}
```

| Field           | Description                                                                                   |
| --------------- | --------------------------------------------------------------------------------------------- |
| `access_token`  | HS256 Bearer JWT. Pass in `Authorization: Bearer <access_token>`.                             |
| `refresh_token` | Use to obtain a new access token when the current one expires.                                |
| `token_type`    | Always `bearer`.                                                                              |
| `expires_in`    | Seconds until the access token expires. Access tokens are valid for **72 hours** (259 200 s). |
| `scope`         | Space-separated list of scopes granted to this token.                                         |
| `workspace_id`  | The workspace this token is bound to.                                                         |

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.causeloop.ai/v1/auth/exchange \
    -H "Content-Type: application/json" \
    -d '{"subject_token": "<workos_access_token>"}'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://api.causeloop.ai/v1/auth/exchange', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ subject_token: workosAccessToken }),
  });
  const { access_token, expires_in } = await response.json();
  ```

  ```python Python theme={null}
  import requests

  resp = requests.post(
      'https://api.causeloop.ai/v1/auth/exchange',
      json={'subject_token': workos_access_token},
  )
  resp.raise_for_status()
  token = resp.json()['access_token']
  ```
</CodeGroup>

### Refreshing a token

When an access token expires, exchange the `refresh_token` for a new one:

```http theme={null}
POST /v1/auth/refresh
Content-Type: application/json

{"refresh_token": "clp_refresh_..."}
```

The response has the same shape as the exchange response, with a fresh `access_token` and a new `refresh_token`.

<Tip>
  Store refresh tokens in `HttpOnly`, `Secure`, `SameSite=Strict` cookies rather than `localStorage` to mitigate XSS token theft.
</Tip>

## Making authenticated requests

Once you have an access token, include it in every request:

```http theme={null}
Authorization: Bearer <access_token>
```

<CodeGroup>
  ```bash curl theme={null}
  curl https://api.causeloop.ai/v1/issues \
    -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  ```

  ```javascript JavaScript theme={null}
  const resp = await fetch('https://api.causeloop.ai/v1/issues', {
    headers: { 'Authorization': `Bearer ${accessToken}` },
  });
  const { data } = await resp.json();
  ```

  ```python Python theme={null}
  import requests

  resp = requests.get(
      'https://api.causeloop.ai/v1/issues',
      headers={'Authorization': f'Bearer {access_token}'},
  )
  resp.raise_for_status()
  issues = resp.json()['data']
  ```
</CodeGroup>

A missing or invalid token returns `401 Unauthorized`:

```json theme={null}
{
  "code": "unauthorized",
  "message": "Missing or invalid Authorization header",
  "details": {}
}
```

## Service accounts (machine-to-machine)

For automation pipelines, CI/CD, and backend integrations that cannot run an interactive WorkOS sign-in, use **service accounts**. Service accounts have their own long-lived bearer tokens with explicit scopes, and do not require user interaction.

<Note>
  Service account endpoints require `workspace:admin` scope.
</Note>

### Create a service account

```http theme={null}
POST /v1/service-accounts
Authorization: Bearer <admin_token>
Content-Type: application/json

{
  "name": "ci-pipeline",
  "scopes": ["issues:read", "issues:write", "patterns:read"]
}
```

**Response (201 Created)**

```json theme={null}
{
  "id": "sa_01abc123",
  "name": "ci-pipeline",
  "scopes": ["issues:read", "issues:write", "patterns:read"],
  "status": "active",
  "workspace_id": "ws_01abc123",
  "created_at": "2024-01-15T10:00:00Z"
}
```

### Issue a token for a service account

```http theme={null}
POST /v1/service-accounts/{id}/tokens
Authorization: Bearer <admin_token>
Content-Type: application/json

{
  "name": "deploy-2024-01",
  "expires_at": "2025-01-15T10:00:00Z"
}
```

**Response (201 Created)**

```json theme={null}
{
  "id": "sat_01abc456",
  "token": "clp_sa_...",
  "name": "deploy-2024-01",
  "expires_at": "2025-01-15T10:00:00Z"
}
```

<Warning>
  The `token` value is shown **only once** at creation time. Store it securely — it cannot be retrieved later. Only a SHA-256 hash is stored by Causeloop.
</Warning>

Use the returned `token` value as the `Authorization: Bearer` token in all API calls made by the service account.

### Manage service account tokens

| Operation              | Endpoint                                                                    |
| ---------------------- | --------------------------------------------------------------------------- |
| List service accounts  | `GET /v1/service-accounts`                                                  |
| Get service account    | `GET /v1/service-accounts/{id}`                                             |
| Delete service account | `DELETE /v1/service-accounts/{id}`                                          |
| List tokens            | `GET /v1/service-accounts/{id}/tokens` (see token metadata, not the secret) |
| Revoke a token         | `DELETE /v1/service-accounts/{id}/tokens/{tok_id}`                          |

## Multi-factor authentication (MFA)

Workspace members can enroll TOTP or SMS factors. MFA is managed through:

| Operation             | Endpoint                           |
| --------------------- | ---------------------------------- |
| List enrolled factors | `GET /v1/mfa/factors`              |
| Enroll a factor       | `POST /v1/mfa/factors`             |
| Verify a factor       | `POST /v1/mfa/factors/{id}/verify` |
| Remove a factor       | `DELETE /v1/mfa/factors/{id}`      |

When a workspace enforces MFA, users who have not enrolled will receive a `403 Forbidden` with code `mfa_required`.

## Single Sign-On (SSO)

Causeloop supports SAML and OIDC SSO connections for enterprise workspaces (Google, Okta, Azure AD, and custom IdPs).

**SSO flow endpoints**

| Endpoint                     | Purpose                                                                  |
| ---------------------------- | ------------------------------------------------------------------------ |
| `GET /v1/auth/sso/providers` | List SSO providers configured for the workspace.                         |
| `GET /v1/auth/sso/authorize` | Initiate an SSO flow — returns a `redirect_url` and `state`.             |
| `GET /v1/auth/sso/callback`  | OAuth/OIDC callback — Causeloop exchanges the code and issues a session. |

**SSO connection management** (requires admin)

| Endpoint                                | Purpose                                     |
| --------------------------------------- | ------------------------------------------- |
| `GET /v1/sso/connections`               | List configured SSO connections.            |
| `POST /v1/sso/connections`              | Create a new SSO connection (SAML or OIDC). |
| `GET /v1/sso/connections/{id}`          | Get connection details.                     |
| `PATCH /v1/sso/connections/{id}`        | Update a connection.                        |
| `DELETE /v1/sso/connections/{id}`       | Delete a connection.                        |
| `POST /v1/sso/connections/{id}/test`    | Test a connection against the IdP.          |
| `GET /v1/sso/connections/{id}/metadata` | Download SAML SP metadata XML.              |

## SCIM provisioning

Causeloop provides a SCIM 2.0 endpoint for automated user and group provisioning from identity providers like Okta and Azure AD.

**Base URL:** `https://api.causeloop.ai/scim/v2/{tenant}`

Authentication uses a dedicated SCIM bearer token (not a user token). Create and manage SCIM tokens:

| Endpoint                      | Purpose                             |
| ----------------------------- | ----------------------------------- |
| `GET /v1/scim/tokens`         | List SCIM tokens for the workspace. |
| `POST /v1/scim/tokens`        | Create a SCIM token.                |
| `DELETE /v1/scim/tokens/{id}` | Revoke a SCIM token.                |

**SCIM resource endpoints**

| Endpoint                                      | Purpose                                       |
| --------------------------------------------- | --------------------------------------------- |
| `GET /scim/v2/{tenant}/ServiceProviderConfig` | Advertise supported SCIM features to the IdP. |
| `GET /scim/v2/{tenant}/Users`                 | List / search users.                          |
| `POST /scim/v2/{tenant}/Users`                | Provision a new user.                         |
| `GET /scim/v2/{tenant}/Users/{id}`            | Get a user.                                   |
| `PUT /scim/v2/{tenant}/Users/{id}`            | Replace a user.                               |
| `PATCH /scim/v2/{tenant}/Users/{id}`          | Partial update (deactivate, rename).          |
| `DELETE /scim/v2/{tenant}/Users/{id}`         | Deprovision a user.                           |
| `GET /scim/v2/{tenant}/Groups`                | List groups.                                  |
| `POST /scim/v2/{tenant}/Groups`               | Create a group.                               |
| `PATCH /scim/v2/{tenant}/Groups/{id}`         | Update group membership.                      |
| `DELETE /scim/v2/{tenant}/Groups/{id}`        | Delete a group.                               |

<Note>
  SCIM tokens are stored as SHA-256 hashes. The plaintext value is returned only at creation.
</Note>
