> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.astropods.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.astropods.com/_mcp/server.

# Authorizing requests

adapter-core ships an auth entry point that answers two questions for an agent that serves its own HTTP surface: who is calling, and may they use this agent? Mount one middleware and every request is identified from the platform's sign-in headers, then checked against the agent's access list before it reaches your routes.

Auth is a separate entry point from the messaging one, so an agent that serves a UI never loads the messaging stack and an agent that calls `serve()` never loads this one.

| Language   | Import                         |
| ---------- | ------------------------------ |
| TypeScript | `@astropods/adapter-core/auth` |
| Python     | `astropods_adapter_core.auth`  |

## When you need it

Use it when your agent receives HTTP requests directly: a [frontend agent](/frontend-agents), or a custom server you serve alongside messaging. A messaging agent on the `web` or `slack` adapter needs nothing, because the platform runs the same check before your code sees a message.

This is the packaged form of [Manually authorize requests](/manual-authz). Read that guide when you need the raw contract, such as for a language the SDK does not cover.

## Install

#### Node.js

```bash
bun add @astropods/adapter-core
# or: npm install @astropods/adapter-core
```

#### Python

```bash
pip install "astropods-adapter-core[auth]"
```

The `auth` extra adds the JWT and HTTP dependencies the check needs. The module imports without it, then fails on the first request it has to verify or authorize.

## Guard every request

Construct an `Authorizer` once, then mount the binding for your framework. Zero-argument construction works in a deployed agent, because every input comes from the environment the platform injects.

#### Node.js

**`server.js`**

```javascript title="server.js"
import express from "express";
import { Authorizer, expressMiddleware } from "@astropods/adapter-core/auth";

const authz = new Authorizer();
const app = express();

app.use(expressMiddleware(authz));

app.get("/", (req, res) => {
  res.json({ user: req.astroPrincipal?.userId ?? "anonymous" });
});

app.listen(80, "0.0.0.0");
```

#### Python

**`main.py`**

```python title="main.py"
from fastapi import FastAPI, Request
from astropods_adapter_core.auth import AstroAuthMiddleware

app = FastAPI()
app.add_middleware(AstroAuthMiddleware)

@app.get("/")
def root(request: Request):
    principal = request.scope.get("astro_principal")
    return {"user": principal.user_id if principal else "anonymous"}
```

One binding covers each way an agent is commonly served. All of them run the same check and return the same status codes.

| Language | Binding                    | Covers                                                            | Principal lands at                 |
| -------- | -------------------------- | ----------------------------------------------------------------- | ---------------------------------- |
| Node     | `expressMiddleware(authz)` | Express, Connect                                                  | `req.astroPrincipal`               |
| Node     | `fastifyHook(authz)`       | Fastify `onRequest` hook                                          | `req.astroPrincipal`               |
| Node     | `honoMiddleware(authz)`    | Hono                                                              | `c.get("astroPrincipal")`          |
| Node     | `withAuth(authz, handler)` | Any Fetch API handler: Next.js route handlers, Bun, Deno, Workers | the handler's second argument      |
| Python   | `AstroAuthMiddleware`      | ASGI: FastAPI, Starlette                                          | `request.scope["astro_principal"]` |
| Python   | `WsgiAuthMiddleware`       | WSGI: Flask, Django                                               | `environ["astro.principal"]`       |
| Python   | `fastapi_dependency()`     | Selected FastAPI routes rather than the whole app                 | the dependency's return value      |

Both Python middlewares default to constructing their own `Authorizer`, so `app.add_middleware(AstroAuthMiddleware)` needs no argument. Pass one when you want to share an instance or override its configuration.

In TypeScript, Express and Fastify know nothing about `astroPrincipal`, so declare it on your framework's request type to read it without a cast. Hono and `withAuth` hand you the principal directly and need no declaration.

## What the guard answers

For every request, the guard identifies the caller, authorizes that identity against the agent's access list, and maps the result onto a status code.

| Outcome                                | Response                                           |
| -------------------------------------- | -------------------------------------------------- |
| The list admits the caller             | `200`, your route runs with the principal attached |
| The list excludes an identified caller | `403`                                              |
| The list excludes an anonymous caller  | `401`                                              |
| The check could not complete           | `503`                                              |

A denial returns a JSON body of the form `{"error": "Forbidden"}` and never reaches your route.

## The principal

| TypeScript | Python    | Value                                                                                      |
| ---------- | --------- | ------------------------------------------------------------------------------------------ |
| `userId`   | `user_id` | The Astropods user id of the caller.                                                       |
| `email`    | `email`   | Present only when the signed claims header was verified and the claim was returned.        |
| `name`     | `name`    | Same condition as `email`.                                                                 |
| `source`   | `source`  | `alb` for an identity from the platform's sign-in layer, `fixed` for a local dev identity. |
| `claims`   | `claims`  | The verified claims, for any other field your identity provider returns.                   |

A request that carries no identity yields no principal. That is not a refusal: the guard sends it on as an anonymous caller, because the access list is what decides whether an open surface admits it.

## Access lists

The SDK authorizes against the `custom` adapter by default, so it checks the access list of the interface your agent serves itself, not the built-in chat's list. Set that list on the agent's **Configure** screen. [Who can talk to an agent](/access-control) covers the entry types and how they combine.

Two behaviors are worth knowing before you mount the middleware:

* An interface with no entries at all admits members of the owning account. Adding any entry turns that fallback off, and the list becomes the only way in.
* **Anyone with an Astropods account** keeps sign-in at the front door and admits every signed-in user, which is the open setting that works with the guard.

**Public** removes sign-in, so no request reaching your agent carries an identity, and it stores no entries on the access list. Every request then authorizes as anonymous with nothing to match, and the guard answers `401`. To serve a public surface, either add an `anyone` entry to the list or do not mount the guard on it.

## Identity verification

The platform's sign-in layer injects two headers. `x-amzn-oidc-identity` carries the user id as a bare string. `x-amzn-oidc-data` carries the same identity as a signed JWT, along with the rest of the claims.

The SDK verifies `x-amzn-oidc-data` by default: it reads `kid` and `signer` from the token header, fetches the ES256 public key for that region, caches it by `kid`, and checks expiry. A header that fails verification is treated as no identity at all, so the request continues as anonymous and the access list decides.

Set `verifyIdentity: false` (`verify_identity=False` in Python) to skip verification and trust `x-amzn-oidc-identity` as sent. Do that only when nothing can reach your agent except through the platform's front door. The principal then carries no `email` or `name`, because those come from the verified claims, and the SDK logs a warning at startup.

## Caching, timeouts, and outages

| Behavior                                         | Value                                                                  |
| ------------------------------------------------ | ---------------------------------------------------------------------- |
| Decision cache                                   | 60 seconds, keyed on the identity and adapter. Denials are cached too. |
| Request timeout                                  | 5 seconds                                                              |
| Platform unreachable, list has an `anyone` entry | Allow, cached for 10 seconds                                           |
| Platform unreachable, no `anyone` entry          | `503`                                                                  |

Because decisions are cached, an access-list edit takes up to one cache window to reach a running agent.

## Local development

`ast project start` does not inject the deploy token, so the `Authorizer` starts in dev mode: it allows every request and logs a warning naming the reason. Set `ASTRO_AUTH_DEV_USER_ID` to have `identify` return a fixed principal, which is enough to exercise the code paths that read the caller.

A deployed agent never falls into dev mode quietly. A token that is present but unreadable fails construction at startup instead of downgrading to allow-all.

## Configuration

Every option has an environment source, and a constructor argument overrides it.

| Variable                 | Meaning                                                                                             | Default                                          |
| ------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `ASTRO_AUTHZ_TOKEN`      | The deploy token, injected into every deployed agent. Supplies the credential and the platform URL. | none, and absent means dev mode                  |
| `ASTRO_AUTH_ADAPTER`     | Which access list to authorize against.                                                             | `custom`                                         |
| `ASTRO_AUTH_CACHE_TTL`   | Decision cache lifetime, in seconds.                                                                | `60`                                             |
| `ASTRO_AUTH_TIMEOUT`     | Per-request timeout, in seconds.                                                                    | `5`                                              |
| `ASTRO_AUTH_DEV_USER_ID` | Fixed identity in dev mode.                                                                         | none                                             |
| `AWS_REGION`             | Region for the identity public key lookup.                                                          | read from the token's `signer` field when absent |

## Lower-level pieces

Call the two halves yourself when a binding does not fit, or when you want the identity without enforcing the list:

#### Node.js

```typescript
const principal = await authz.identify(headers);   // Principal | null
const decision = await authz.authorize(principal); // { allowed, userId }
```

#### Python

```python
principal = authz.identify(headers)    # Principal | None
decision = authz.authorize(principal)  # Decision(allowed, user_id)
```

Both calls are synchronous. On an async server, use `guard_async(authz, headers)`, which runs the sequence off the event loop.

`guard(authz, headers)` runs the sequence and returns the status code a binding would have sent, which is the piece to reuse when wiring a framework the SDK does not cover. Below that sit `AlbIdentityVerifier`, `AuthorizeClient`, `DecisionCache`, and `decodeDeployToken` for supplying your own identity or transport.

## Next steps

* [Serve a frontend from your agent](/frontend-agents): the interface this check protects
* [Access control](/access-control): the entry types an access list accepts
* [Manually authorize requests](/manual-authz): the underlying contract, for a language the SDK does not cover