Authorizing requests

Check who is calling the HTTP surface your agent serves
View as Markdown

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.

LanguageImport
TypeScript@astropods/adapter-core/auth
Pythonastropods_adapter_core.auth

When you need it

Use it when your agent receives HTTP requests directly: a frontend agent, 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. Read that guide when you need the raw contract, such as for a language the SDK does not cover.

Install

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

LanguageBindingCoversPrincipal lands at
NodeexpressMiddleware(authz)Express, Connectreq.astroPrincipal
NodefastifyHook(authz)Fastify onRequest hookreq.astroPrincipal
NodehonoMiddleware(authz)Honoc.get("astroPrincipal")
NodewithAuth(authz, handler)Any Fetch API handler: Next.js route handlers, Bun, Deno, Workersthe handler’s second argument
PythonAstroAuthMiddlewareASGI: FastAPI, Starletterequest.scope["astro_principal"]
PythonWsgiAuthMiddlewareWSGI: Flask, Djangoenviron["astro.principal"]
Pythonfastapi_dependency()Selected FastAPI routes rather than the whole appthe 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.

OutcomeResponse
The list admits the caller200, your route runs with the principal attached
The list excludes an identified caller403
The list excludes an anonymous caller401
The check could not complete503

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

The principal

TypeScriptPythonValue
userIduser_idThe Astropods user id of the caller.
emailemailPresent only when the signed claims header was verified and the claim was returned.
namenameSame condition as email.
sourcesourcealb for an identity from the platform’s sign-in layer, fixed for a local dev identity.
claimsclaimsThe 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 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

BehaviorValue
Decision cache60 seconds, keyed on the identity and adapter. Denials are cached too.
Request timeout5 seconds
Platform unreachable, list has an anyone entryAllow, cached for 10 seconds
Platform unreachable, no anyone entry503

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.

VariableMeaningDefault
ASTRO_AUTHZ_TOKENThe deploy token, injected into every deployed agent. Supplies the credential and the platform URL.none, and absent means dev mode
ASTRO_AUTH_ADAPTERWhich access list to authorize against.custom
ASTRO_AUTH_CACHE_TTLDecision cache lifetime, in seconds.60
ASTRO_AUTH_TIMEOUTPer-request timeout, in seconds.5
ASTRO_AUTH_DEV_USER_IDFixed identity in dev mode.none
AWS_REGIONRegion 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:

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