Authenticate programmatic access to the Halo API using OAuth 2.0 Bearer tokens issued by Okta.

## Overview

Halo exposes a REST API through the API-Access service. All API requests must include a valid Bearer token in the `Authorization` header. Tokens are obtained from Okta using the OAuth 2.0 Client Credentials flow (machine-to-machine).

This guide walks through configuring Okta as the Identity Provider for the Halo API-Access service, then testing the integration with Bearer token authentication.

## Prerequisites

- A running Halo deployment with API-Access service configured
- An Okta organization with admin access
- Okta authorization server configured for Halo (see [Okta Authorization Server](/halo/okta-authorization-server))
- Network access from the API client to both Okta and the Halo API endpoint

---

## Setup

### Register an API client in Okta

#### Create a machine-to-machine application

1. Login to the Okta Admin Console.
2. Navigate to `Applications` -> `Applications` -> `Create App Integration` -> `API Services`.

![Screenshot: Create API Services application in Okta](/.attachments/image-8f18c073-8992-467d-a9b7-a7fb68ba9d31.png)

3. Provide an appropriate App integration name: e.g. `Glasswall Halo API`

![Screenshot: API service app name in Okta](/.attachments/image-fb4dbe1a-8de1-4336-a21b-0eb3d1a54564.png)

#### Disable DPoP (proof of possession)

4. Navigate to the application's `General` tab -> `General Settings` section -> click `Edit`.
5. Under **Proof of possession**, untick `Require Demonstrating Proof of Possession (DPoP) header in token requests`.
6. Save.

> **Note:** Okta enables DPoP by default for API Services applications. DPoP binds tokens to the client's cryptographic key, preventing token replay attacks. However, the Halo API-Access service does not currently support DPoP token validation, so this must be disabled for Bearer token authentication to work.

![Screenshot: Disable DPoP in Okta application settings](/.attachments/image-050bb573-8519-426f-a4bd-2395edaaf976.png)

#### Configure client credentials

7. Note the `Client ID` and generate a `Client Secret`.

    ```sh
    export CLIENT_ID="<your-client-id>"
    export CLIENT_SECRET="<your-client-secret>"
    ```

> **Security Note:** Store client secrets securely using a secrets manager (e.g., Azure KeyVault). Never commit secrets to source control.

![Screenshot: Client ID and secret from Okta application](/.attachments/image-7d9c8db3-dc5b-4dd8-aa1f-632c04da0429.png)

#### Configure roles (optional)

8. If your API client needs write access to license management endpoints, ensure the `role` claim is configured on the authorization server with the `Admin` value mapped to your client (see [API Roles](/halo/okta-authorization-server#api-roles-role-claim)).

> **Note:** Without the `role` claim, the API defaults to the `User` role. See [API Roles to Action Mapping](/halo/api-roles-to-action-mapping) for details on what each role can access.

#### Note issuer URI and audience

9. From the authorization server configured in the [prerequisite step](/halo/okta-authorization-server):

    ```sh
    export OKTA_ISSUER_URI="https://<your-okta-domain>/oauth2/<authorization-server-id>"
    export VALID_AUDIENCE="api://halo"
    ```

#### Add access policy for API client

10. Navigate to the authorization server's `Access Policies` tab (`Security` -> `API` -> select your authorization server -> `Access Policies`).
11. Add a new access policy:
    - Name: e.g. `API Client Access`
    - Description: e.g. `Access policy for Halo API clients`
    - Assign to: the API Services client created above (search by application name `glasswall-halo-api` from step 2)
12. Add a rule:
    - Name: e.g. `Allow API Clients`
    - Grant type: `Client Credentials`
    - Leave other settings as defaults or adjust token lifetime as needed.

> **Note:** Access policies for the Client Credentials grant control which **clients** (by `client_id`) can request tokens. Group membership does not apply because there is no user context in this flow. Secure access to the API by restricting who has the `client_id` and `client_secret` via your secrets manager.

![Screenshot: API access policy on authorization server](/.attachments/image-12ebf516-b690-4874-870d-bf4c1643bb42.png)

### Configure API-Access service for Okta

The `cdrplatform-api-access` Helm chart must be configured to validate Bearer tokens issued by your Okta authorization server. The chart exposes authentication settings under the `configuration` values key.

#### Deploy the configuration

> **Note:** The `Authority` and `ValidIssuer` must both be set to the Okta issuer URI. The API-Access service uses `Authority` to discover the JWKS (JSON Web Key Set) for token signature validation, and `ValidIssuer` to verify the `iss` claim in the token.

```sh
export HALO_DOMAIN=<your-halo-domain>
helm upgrade --install cdrplatform-api-access cdrplatform-api-access --reuse-values \
  --set configuration.AuthenticationScheme="Bearer" \
  --set configuration.Authentication__Schemes__Bearer__Authority="${OKTA_ISSUER_URI:?}" \
  --set configuration.Authentication__Schemes__Bearer__ValidIssuer="${OKTA_ISSUER_URI:?}" \
  --set configuration.Authentication__Schemes__Bearer__ValidAudiences__0="${VALID_AUDIENCE:?}" \
  --set strategy.type="Recreate" \
  --set ingress.enabled=true \
  --set ingress.tls.enabled=true \
  --set ingress.tls.domain="${HALO_DOMAIN:?}" \
  --atomic
```

#### Multiple audiences (optional)

If multiple API clients or applications need to access the Halo API with different audiences, add additional `ValidAudiences` entries:

```sh
  --set configuration.Authentication__Schemes__Bearer__ValidAudiences__0="api://halo-api" \
  --set configuration.Authentication__Schemes__Bearer__ValidAudiences__1="<second-client-id>" \
  --set configuration.Authentication__Schemes__Bearer__ValidAudiences__2="<third-client-id>"
```

---

## Halo API usage

### Obtain a bearer token

#### Client Credentials flow (machine-to-machine)

Use this flow for automated systems, CI/CD pipelines, and service-to-service integrations where no user interaction is required.

```sh
export TOKEN_ENDPOINT="${OKTA_ISSUER_URI}/v1/token"
```

Request a token:

```sh
curl -s -X POST "${TOKEN_ENDPOINT}" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=${CLIENT_ID}" \
  -d "client_secret=${CLIENT_SECRET}" \
  -d "scope=halo.api"
```

Example response:

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "halo.api"
}
```

Extract the token:

```sh
export ACCESS_TOKEN=$(curl -s -X POST "${TOKEN_ENDPOINT}" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=${CLIENT_ID}" \
  -d "client_secret=${CLIENT_SECRET}" \
  -d "scope=halo.api" | jq -r '.access_token')
```

### Authenticate API requests

Include the Bearer token in the `Authorization` header of every API request:

```sh
export HALO_API_URL="https://<your-halo-domain>"
```

#### Verify authentication

Test with a protected endpoint. A request without a token should return `401`, and a request with a valid token should return `200`:

```sh
# Without token - expect 401
curl -s -o /dev/null -w "%{http_code}" \
  "${HALO_API_URL}/api/v1/policies"

# With token - expect 200
curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  "${HALO_API_URL}/api/v1/policies"
```

#### Example: Submit a file for processing

```sh
curl -s -X POST \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -F "file=@/path/to/document.pdf" \
  "${HALO_API_URL}/api/v1/rebuild/file" \
  --output rebuilt_document.pdf
```

## Troubleshooting

| Symptom | Cause | Resolution |
|---|---|---|
| `401 Unauthorized` | Token expired or invalid | Request a new token. Verify `CLIENT_ID` and `CLIENT_SECRET` are correct. |
| `401 Unauthorized` with valid token | Audience mismatch | Ensure `ValidAudiences` in API-Access matches the `aud` claim in the token. |
| `403 Forbidden` with `AdminRoleMissing` | Missing `Admin` role in token | Configure the `role` claim on the authorization server (see [API Roles](/halo/okta-authorization-server#api-roles-role-claim)). Required for license management write operations. |
| Token request fails | Incorrect token endpoint URL | Verify the authorization server ID and Okta domain. Check `/.well-known/openid-configuration`. |
| `invalid_scope` error | OIDC scopes used with Client Credentials | Use the custom `halo.api` scope instead of `openid`, `profile`, or `email`. |
| `invalid_dpop_proof` error | DPoP not disabled | Disable DPoP on the API Services application (see [Disable DPoP](#disable-dpop-proof-of-possession)). |
| Network timeout | Firewall or proxy blocking | Ensure the API client can reach both the Okta token endpoint and the Halo API endpoint. |