# Protect an API

Require a valid access token issued for one API, write rules for who may call each route, and look up how route rules are matched.

Zero-Code checks every call to an API on the node, before the request reaches your code. A caller without a valid access token for this API is denied, and your service does not verify tokens, fetch keys or parse claims.

Use this guide for a service that programs call. For a service people open in a browser, see [Protect a web app](/zero-code/protect-ui).

## Before you begin

You need:

- A cluster with Zero-Code installed. See [Install Zero-Code on a cluster](/zero-code/install).
- The callers of the API: each one is a [machine-to-machine application](/applications/machine-to-machine) or an AI agent.

<Steps>

### Open the workload

In the AuthFI console, open **Zero-Code > Kubernetes** and select the cluster. On the **Workloads** tab, find the workload and choose **Classify** on its row.

### Classify it as an App that serves an API

1. The step asks what the workload is. Choose **App**.
2. Under **What does it serve?**, turn on **API**.
3. Choose **Continue to Protect**.

The console confirms that the workload is classified and opens the Protect step, which asks who may reach the workload.

### Turn on the API door

Turn on **API · software calls it**. Every call must then carry a token issued for this service's _audience_, the name a token is issued for.

### Write the route rules

A _route rule_ says who may call a route. With the API switch on, the Protect step shows **Route rules**.

AuthFI drafts rules from the routes and callers it observed, so you start from what is really called and by whom. Choose **Fill in the draft** to load them, then correct them.

To add a rule:

1. Choose the method and enter the path, for example `/orders` or `/admin/*`.
2. Choose **Add route**.
3. On the new row, choose **Signed in** or **Anyone**.
4. Choose **+ Requirement** and pick what the caller must hold.

Requirements are chosen from your project's own roles, groups and permissions, not typed in. Each control is described in the [route rule reference](#route-rule-reference).

### Choose where the service may connect

Under **What it may reach**, the console lists each destination it saw the service connect to. Tick the destinations the service needs.

To add a destination the service will need, enter its address or network and port, for example `10.0.0.0/8:5432`, and choose **Allow destination**.

### Save the draft and copy the audience

Choose **Save draft**. The console confirms with **Draft saved**: the rules are recorded, not enforced, until you turn protection on.

Saving gives the service its own API identity, registered as an [application](/applications/types) in your project. The Protect step now shows the audience. On the workload's page, under **How it is reached**, the **API** card has **Copy audience**.

### Grant each caller access to the API

A caller must be granted access to the API before a token for it is issued. On the **API** card, choose **Grant an application access to it**. For a machine-to-machine application, the control on the application's page is **Allow an API**.

A caller then asks for a token by naming the audience in `resource`.

| Caller      | How it gets a token for this API                                    |
| ----------- | ------------------------------------------------------------------- |
| A service   | [Client credentials](/applications/machine-to-machine)              |
| An AI agent | [Agent sign-in](/agents/verify#give-the-agent-a-token-for-your-api) |

A token issued for another API is denied here.

### Turn on protection

While the draft is saved and protection is off, every request is forwarded, and each one that would have been denied is recorded with its reason. Read the [audit record](/security/audit) and correct the rules until it reads right.

Then open the Protect step again and choose **Turn on protection…**, and confirm with **Turn on protection**. The button is available after 24 hours of watching.

The console confirms that protection is turned on. It applies on the nodes running the workload within 30 seconds. From then on, a call is denied unless its access token was issued for this API, including callers that work today. The **API** card shows **token required**.

</Steps>

## Check that it works

Get a token for the API as a granted machine-to-machine application.

```bash
curl https://acme.authfi.io/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d resource="$API_CLIENT_ID"
```

`$CLIENT_ID` and `$CLIENT_SECRET` belong to the calling application. `$API_CLIENT_ID` is the audience you copied.

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6Ii4uLiJ9...",
  "token_type": "Bearer",
  "expires_in": 900
}
```

Call a route that the caller's rule allows, with `access_token` as a `Bearer` token. The request reaches your service. Call the same route with no token, and the request is denied with `missing_token` before it reaches your service.

## What is checked on each request

The node makes these checks in order, before your service sees anything.

| Check                                                  | Denied as             |
| ------------------------------------------------------ | --------------------- |
| There is a `Bearer` token                              | `missing_token`       |
| It is signed by your project, and has not expired      | `invalid_token`       |
| The AI agent or session behind it has not been revoked | `invalid_token`       |
| It is an access token, not an ID token                 | `not_an_access_token` |
| It was issued for this API                             | `audience_mismatch`   |
| The route's rule is met                                | `policy_denied`       |

Revocation is checked on every request. Revoke an AI agent or end a session, and the next request is denied, without waiting for the token to expire.

## What a denied request receives

```text
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer realm="authfi", error="invalid_token"
X-Authfi-Ref: 7c1f0a2e
Content-Type: application/json

{ "error": "authfi_audience_mismatch", "message": "This credential was issued for a different service.", "ref": "7c1f0a2e" }
```

`ref` identifies this denial in the [audit record](/security/audit), with the caller, the route and the reason. Give it to whoever asks why they were denied.

To see which rules deny most, open **Security > Signals**. See [What is recorded](/zero-code/enforcement#what-is-recorded).

## Read who is calling

As for a web app, the request reaches your service with the caller in [headers](/zero-code/protect-ui#read-who-is-signed-in): `X-AuthFI-User-ID`, `X-AuthFI-Groups` and `X-AuthFI-Roles`. A request from an AI agent also carries `X-AuthFI-Agent-ID`.

## Route rule reference

A rule names a method and a path, and what a caller must hold to call it. You edit rules under **Route rules** in the Protect step.

### Controls

| Control                    | Meaning                                                                  |
| -------------------------- | ------------------------------------------------------------------------ |
| Method                     | `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, or **Any method**               |
| Path                       | An exact path, or a prefix ending in `/*`. A path starts with `/`.       |
| **Signed in** / **Anyone** | Who may call the route                                                   |
| **Permissions · all of**   | The caller holds **all** of these                                        |
| **Groups · any of**        | The caller is in **any** of these                                        |
| **Roles · any of**         | The caller holds **any** of these                                        |
| **Require a recent MFA**   | The person signed in with multi-factor authentication (MFA) or a passkey |

The last four are under **+ Requirement**. A caller needs every permission listed and any one of the groups or roles.

### Example

| Method     | Path             | Requires                                                   |
| ---------- | ---------------- | ---------------------------------------------------------- |
| `POST`     | `/v1/refunds`    | Group `finance`, permission `refunds.create`, a recent MFA |
| `GET`      | `/v1/payments/*` | Group `finance` or `support`                               |
| Any method | `/internal/*`    | Role `platform-admin`                                      |
| Any method | `/*`             | **Signed in**: a valid token for this API                  |

### Which rule decides a request

The first rule that matches a request decides it.

More specific rules are tried first: an exact path before a prefix, and a longer prefix before a shorter one. In the example, `/v1/refunds` is decided by its own rule, not by `/*`.

### When no rule matches

The list of rules ends with a fixed row, **everything else**. Once the workload is protected, a request that matches no rule above it is denied. An API with rules is closed by default, and you open the routes you mean to open.

### Paths that are always let through

These paths are answered without a token, so that health checks and sign-in keep working.

| Kind           | Paths                                                                                                           |
| -------------- | --------------------------------------------------------------------------------------------------------------- |
| Health         | `/health`, `/healthz`, `/livez`, `/ready`, `/readyz`                                                            |
| Discovery      | `/.well-known/*`                                                                                                |
| Sign-in        | `/login`, `/register`, `/signup`, `/callback`, `/oauth/callback`, `/auth/*` equivalents, and `POST` to `/token` |
| Browser basics | `/favicon.ico`, `/robots.txt`, `/static/*`, `/assets/*`, any `OPTIONS` request                                  |

<Warning title="These paths are public">
  Do not serve anything private under these paths. A request to one of them reaches your service without a token.
</Warning>

## Hand a service back

To remove protection from a service, open **Zero-Code > Kubernetes**, select the cluster and open the **Services** tab. The tab lists what the node agent found listening in the cluster, and whether each service is protected.

Choose **Unprotect** on the service's row and confirm with **Hand it back**. Every policy for the service is deactivated, and its port reopens the next time the nodes fetch their rules.

**Protect** on the same row does the opposite, with no confirmation. The service is denied by default from the next time the nodes fetch their rules: every caller without a grant is dropped in the kernel.

## Troubleshooting

| What you see                                                                | Cause                                                                | Fix                                                              |
| --------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `missing_token`                                                             | The request has no `Bearer` token                                    | Send the access token as a `Bearer` token                        |
| `invalid_token`                                                             | The token is not signed by your project, has expired, or was revoked | Get a new token from your project                                |
| `not_an_access_token`                                                       | The request carries an ID token                                      | Send the access token                                            |
| `audience_mismatch`                                                         | The token was issued for another API                                 | Ask for a token with this API's audience in `resource`           |
| `policy_denied`                                                             | The caller does not meet the route's rule                            | Compare the caller's roles, groups and permissions with the rule |
| `invalid_target` when asking for a token                                    | The caller has no grant on the API                                   | Grant the caller access to the API                               |
| "A path starts with /" when adding a route                                  | The path does not start with `/`                                     | Enter a path such as `/orders` or `/admin/*`                     |
| **Turn on protection…** is unavailable, with **Watched for under 24 hours** | The workload has been watched for less than 24 hours                 | Save the draft. It records what it would deny in the meantime.   |

## Next steps

<Cards>
  <Card href="/applications/machine-to-machine" title="Service to service">
    Get a token for one API with the client credentials grant.
  </Card>
  <Card href="/agents/verify" title="Accept agents at your own API">
    How an AI agent gets a token for your API, and what the token carries.
  </Card>
  <Card href="/zero-code/enforcement" title="How enforcement works">
    Where each decision is made, and what monitor mode records.
  </Card>
  <Card href="/security/audit" title="The audit record">
    Find a denial by its reference.
  </Card>
</Cards>
