Manually authorize requests
When you deploy with the default web or slack adapter, the messaging sidecar handles authorization automatically: every incoming request is checked against the deployment’s grants table before it reaches your agent. If you’re running a frontend agent or a custom HTTP server inside the messaging container, there’s no sidecar to do this for you, but the platform still issues the same credentials, and you can call the same endpoint yourself.
This guide explains the contract and shows how to replicate it.
What the sidecar is doing
For every inbound request, the messaging sidecar:
- Extracts an identity from the request (the OIDC user_id for web, the Slack user/team for slack).
- Calls
GET /api/v1/deployments/authorizeon astro-server with that identity and the adapter name, authenticated by the deploy token. - Allows the request if the server returns
allowed: true; denies it otherwise. - Caches the answer for ~60s so chatty sessions don’t pay the round-trip on every event.
- Falls back to the deploy token’s
anyone_adaptersclaim if the server is unreachable, so an outage doesn’t take down open-grant deployments.
When you handle requests directly, you reproduce these five steps inside your agent.
Where the identity comes from
A frontend agent sits behind the platform’s OIDC layer. After a user signs in, every request that reaches your container carries the signed-in user’s platform ID in a request header:
This header is injected by the OIDC layer after sign-in and is the only identity input you need for a web frontend. You read it off the incoming request — you do not mint, decode, or validate it yourself. If it’s absent, the request is unauthenticated (treat as anonymous; see the anyone-grant note below).
Inputs the platform gives you
Every deployed agent receives the following environment variable:
The token is opaque to you, but two claims inside it are useful:
Decode the token once at startup to read iss. You do not need to validate its signature, because astro-server validates the token again on every authorize call.
Where the web identity comes from
For a web agent, the Application Load Balancer (ALB) that sits in front of your container runs the OIDC login and forwards the result as request headers. Two headers carry the identity.
x-amzn-oidc-identity holds the OIDC subject, which is the platform user id. Pass this value to the authorize call as identity_id, together with identity_type=user. It contains the subject only, with no email or any other profile field.
x-amzn-oidc-data holds a signed JWT with the full set of claims for the authenticated user, including sub, email, and email_verified. The authorize endpoint never returns the email, so this header is the only place to get it or any other profile field. Which claims are present depends on the scopes your identity provider returns. To read the email, decode the payload segment (the middle of the three parts separated by dots) and read the claim:
Node.js
Python
The ALB also injects x-amzn-oidc-accesstoken, the raw access token from the identity provider. Most agents do not need it.
These headers carry tokens that expire quickly and that the ALB refreshes, so read them on every request rather than persisting or caching the decoded identity beyond the request.
Keep the two JWTs distinct. ASTRO_AUTHZ_TOKEN is the deploy token that you present to astro-server, and its sub is the deployment id. x-amzn-oidc-data is the identity token that the ALB presents to you, and its sub is the user id. They are separate tokens with separate subjects, so do not assume that a sub means the same thing in both.
You can read the claims in x-amzn-oidc-data without verifying its signature only because the ALB strips any copy of these headers sent by the client and sets its own. Nothing downstream can forge them as long as every request reaches your container through the ALB. If your container can be reached directly, so that a request can arrive without passing through the ALB, verify the signature before you trust any claim. Fetch the public key from https://public-keys.auth.elb.<region>.amazonaws.com/<kid>, where the region comes from the signer ARN in the JWT header and kid comes from that same header, then verify with the ES256 algorithm.
When you bring up a frontend agent, a temporary diagnostic route that echoes these headers and their decoded claims helps confirm that the ALB is forwarding identity as expected. Remove or gate such a route before production, because it is unauthenticated and exposes token claims.
The authorize call
Substitute the server URL with the iss claim from your ASTRO_AUTHZ_TOKEN, and pass the raw token as a Bearer credential:
Query parameters:
For a web frontend you only set adapter=web and the identity from the x-amzn-oidc-identity header:
The remaining two parameters exist only for custom adapters that map a non-web identity (e.g. Slack). Ignore them for a web frontend:
Response (200):
allowedis the final decision.user_idis the resolved platform user id when the server could resolve one (foridentity_type=userit’s the input echoed back; forslackit’s the linked platform user, empty when no mapping exists). Only present whenallowed: true.
Treat 4xx as malformed input (don’t retry) and 5xx as transient (retry once, then fail-closed).
Reference implementation
This is the whole loop for a web frontend: an authorize client, plus a guard that reads x-amzn-oidc-identity off the request, calls the client, and allows or denies. Drop the guard in front of every protected route.
Node.js
Python
If you author a custom adapter instead of a web frontend, the identity won’t be in x-amzn-oidc-identity — source it from your adapter’s transport (e.g. a Slack event payload) and pass adapter/identity_scope accordingly. Everything else about the call is identical.
Caveats worth knowing
- Cache for ~60s. Without a cache, every page navigation pays a round-trip to astro-server. The sidecar caches per
(identity_type, identity_id, adapter, identity_scope)for 60 seconds; do the same. Grant edits take up to a minute to propagate, which is acceptable. - Fail-closed on timeouts. A 5s timeout is the platform default. Treat any error as a denial unless the adapter is in the token’s
anyone_adaptersclaim. In that case a server outage should not lock everyone out of an open deployment. Cap the degraded-mode TTL low (10s or so) so recovery is reflected promptly. - Anonymous is only valid with an
anyonegrant. Sending emptyidentity_type+ emptyidentity_idis allowed by the server only when the adapter is publicly granted. If your UI has a public route, route it through the same authorize call with empty identity instead of branching around it. - Use the resolved
user_iddownstream. For Slack, the linked platform user id is what trace attribution and observability buckets are keyed on. For web, it’s just the input echoed back. Either way, forwardresult.user_idto your downstream telemetry rather than the raw input. - No token, no platform. In local dev (
ast project start),ASTRO_AUTHZ_TOKENis not set and the ALB identity headers (x-amzn-oidc-identityandx-amzn-oidc-data) are absent. Returnallowed: trueso devs aren’t blocked. The platform only injects the token and the identity headers in deployed builds.
When you don’t need this
If you’re using the default --adapter web flow, the sidecar already handles all of this for you and your agent code never sees ASTRO_AUTHZ_TOKEN directly. This guide is only relevant when:
- You set
agent.interfaces.frontend: trueand serve your own UI. - You’re authoring a custom adapter and want to wire authorize calls into the request path yourself.
- You’re building a server-to-server integration that talks to astro-server on the deployment’s behalf.