> 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 (Node)

`@astropods/adapter-core/auth` 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.

Python has its own binding set on [Authorizing requests (Python)](/adapters/python/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

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

## 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.

**`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");
```

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

| Binding                    | Covers                                                            | Principal lands at            |
| -------------------------- | ----------------------------------------------------------------- | ----------------------------- |
| `expressMiddleware(authz)` | Express, Connect                                                  | `req.astroPrincipal`          |
| `fastifyHook(authz)`       | Fastify `onRequest` hook                                          | `req.astroPrincipal`          |
| `honoMiddleware(authz)`    | Hono                                                              | `c.get("astroPrincipal")`     |
| `withAuth(authz, handler)` | Any Fetch API handler: Next.js route handlers, Bun, Deno, Workers | the handler's second argument |

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

| Field    | Value                                                                                      |
| -------- | ------------------------------------------------------------------------------------------ |
| `userId` | The Astropods user id of the caller.                                                       |
| `email`  | Present only when the signed claims header was verified and the claim was returned.        |
| `name`   | Same condition as `email`.                                                                 |
| `source` | `alb` for an identity from the platform's sign-in layer, `fixed` for a local dev identity. |
| `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` 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:

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

`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