# Authentication

> A person's session through your identity provider, or a machine key with your Access service token.

A request to the API is made either by a **person**, signed in through your own identity provider, or by a **machine**, with an API key. Both are checked on every request, at your tenant’s own host.

## People: your identity provider, with a second factor

A person signs in at `platform.atlanticagenticlabs.com` with your tenant ID, then through your identity provider (Microsoft Entra ID, Google, Okta, GitHub, GitLab, or any SAML or OIDC provider). Cloudflare Access verifies the sign-in and sends LightHouse a signed assertion with each request (`Cf-Access-Jwt-Assertion`, or the `CF_Authorization` cookie).

LightHouse then:

1. requires proof of a second factor, from the provider or from the sign-in policy’s own audit, or refuses with `403 mfa_required`;
2. requires the provider to be one your tenant allows, and the person to match a grant in your access rules, or refuses with `403 not_authorised`;
3. gives the person the highest role their grants allow.

Sign-ins and refusals go to your audit log. People’s requests from scripts are not the intended path: use a machine key.

## Machine keys

A machine key lets your own tools read the API and MCP.

| Property | Rule                                                                                                       |
| -------- | ---------------------------------------------------------------------------------------------------------- |
| Format   | `aalcp-<8 hex>-<43 characters>`, sent as `Authorization: Bearer aalcp-...`                                 |
| Role     | `viewer` or `content_viewer`. Never `handler` or `client_admin`, so a leaked key cannot change who gets in |
| Expiry   | Required, at most 365 days                                                                                 |
| Scope    | One tenant. At any other tenant’s address the key does not exist (`401`)                                   |
| Storage  | Shown once. LightHouse keeps only its SHA-256, so a copy of the database grants nothing                    |

### Creating a key

**Settings → API keys** (a `client_admin`, signed in): a label, the role, and an expiry of 30, 90 or 180 days or a year. Copy it at once; it is never shown again. By API, as a person: `POST /api/v1/keys` with `{"label": "bi-export", "role": "viewer", "expires_at": "2027-01-31T00:00:00Z"}`. See [Issue Key](/api/reference/access-and-keys/#issue-key).

### Rotating and revoking

Issue the new key, move your tool to it, then revoke the old one: **Settings → API keys → Revoke**, or `DELETE /api/v1/keys/{key_id}`. A revocation is recorded, not deleted, so the audit trail keeps who revoked it and when. The page also shows each key’s last use.

## The Access service token at the edge

Your tenant’s host sits behind Cloudflare Access. A machine passes it with your tenant’s **machine access token**, a Cloudflare Access service token Atlantic issues with your tenant, sent as two headers beside the key:

```text
CF-Access-Client-Id: <client id>
CF-Access-Client-Secret: <client secret>
```

**Settings → API keys** shows the token’s expiry and when it was last renewed. Atlantic renews it before it expires; if it has lapsed, ask Atlantic.

**curl**

```bash
curl -s "https://$AAL_TENANT.atlanticagenticlabs.com/api/v1/me" \
  -H "Authorization: Bearer $AAL_KEY" \
  -H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
  -H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET"
```

**Python**

```python
import os

import httpx

headers = {
    "Authorization": f"Bearer {os.environ['AAL_KEY']}",
    "CF-Access-Client-Id": os.environ["CF_ACCESS_CLIENT_ID"],
    "CF-Access-Client-Secret": os.environ["CF_ACCESS_CLIENT_SECRET"],
}
r = httpx.get(f"https://{os.environ['AAL_TENANT']}.atlanticagenticlabs.com/api/v1/me", headers=headers)
print(r.status_code, r.json()["viewer"]["role"])
```

**TypeScript**

```ts
const res = await fetch(`https://${process.env.AAL_TENANT}.atlanticagenticlabs.com/api/v1/me`, {
  headers: {
    Authorization: `Bearer ${process.env.AAL_KEY}`,
    "CF-Access-Client-Id": process.env.CF_ACCESS_CLIENT_ID!,
    "CF-Access-Client-Secret": process.env.CF_ACCESS_CLIENT_SECRET!,
  },
});
console.log(res.status, (await res.json()).viewer.role);
```

## What a machine key may never do

A key is refused on every route that does any of the following: with `403 person_required` where the route acts only for a signed-in person, or `403 role_required` where it needs `handler` or `client_admin`, which no key can hold.

- **approves or decides**: approvals and every handler command;
- **pays**: payment methods, checkout, top-ups, invoices marked paid;
- **raises or changes a limit**: spending limits, caps, pre-approvals;
- **administers**: access rules, keys, connectors, branding, where agents run, the library’s **Add to deployment**.

This is what stops an AI agent holding a key from approving its own work, spending, or loosening its own limits. See [API security](/security/api/).

## Delegated sign-in for tools

A short-lived token a person approves in their own portal (a device code for a command line, OAuth for an MCP client) is \[Coming].

Source: https://docs.atlanticagenticlabs.com/api/authentication/
