---
title: "Stamp Identity at the Door"
description: "Replace the throwaway header door from 2.3 with a real ordered auth walk that derives the caller's identity, and tier, from a session, then feeds the per-tier playbook."
canonical_url: "https://vercel.com/academy/building-agents-with-eve/stamp-identity"
md_url: "https://vercel.com/academy/building-agents-with-eve/stamp-identity.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-06-30T16:00:21.598Z"
content_type: "lesson"
course: "building-agents-with-eve"
course_title: "Building Agents with eve"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Stamp Identity at the Door

# Stamp Identity at the Door

Back in 2.3 we did something shady to get the per-tier playbook working: we trusted an `x-shop-tier` header. Anyone who knew to send `x-shop-tier: pro` got the pro discount, no questions asked. It was a deliberate stand-in, and we flagged it as one. Now that the dispatcher has real front doors, the web and Slack, it's time to make identity real too.

The job of a door is to answer one question before any work happens: *who is this?* In eve that's **route auth**, a policy on the channel that runs before the model does a thing. It decides who's allowed in, and it produces the caller's identity, the same `ctx.session.auth` the playbook reads. Auth helps us know our customer.

## Outcome

The dispatcher derives the caller's identity from a real session, stamps their `tier` from the shop's records, and feeds it to the per-tier playbook, with no way for a caller to set their own tier.

## Hands-on exercise

The `auth` option on the channel takes a list of functions eve walks **in order**. Each one does one of three things:

- returns a `SessionAuthContext` → accept the caller, stop the walk
- returns `null` → "not my caller," fall through to the next entry
- throws → reject with a specific status

If every entry falls through, the request gets a `401`. That ordered walk is the whole model: put your real auth first, then the framework's dev and OIDC helpers as catch-alls.

You need something that turns a request into a known customer. In a real app that's your auth provider (Clerk, Auth.js, your own OIDC). For the course, drop in a small stand-in:

```bash
curl -f --create-dirs -o agent/lib/auth.ts https://raw.githubusercontent.com/vercel-labs/bike-shop-agent/main/agent/lib/auth.ts
```

It exposes one function, `getCustomer(request)`, that reads a session cookie and returns the customer on file, *including their tier*:

```ts title="agent/lib/auth.ts (excerpt)"
export interface Customer {
  readonly id: string;
  readonly tier?: "member" | "pro";
}

// Reads the `shop_session` cookie and looks the customer up server-side.
export function getCustomer(request: Request): Customer | null;
```

The detail that matters: the tier is a property of the customer record, looked up server-side, not a value the request carries. That's the entire fix for 2.3's vulnerability.

Rewrite `agent/channels/eve.ts` so `appAuth` uses it:

- Call `getCustomer(request)`. No customer? Return `null` and fall through.
- Otherwise return a `SessionAuthContext`: the customer's `id` as `principalId`, `principalType: "user"`, an `authenticator` and `issuer` label, and `attributes.tier` *only when the record has one*.

\*\*Note: Identity is set once, and flows everywhere\*\*

You stamp the caller's identity in this one file, and it rides the whole session as `ctx.session.auth.current`. The playbook from 2.3 reads `attributes.tier` from it. A tool could read `principalId` to scope data. You don't thread identity through your code, the door stamps it, the runtime carries it.

## Try It

Restart the server. Testing a tier means calling as a specific customer, which means sending the `shop_session` cookie, so we use `curl` here rather than the dev TUI (the TUI always runs as a local dev caller with no cookie, the plain desk).

One thing about the API first: a `POST` to `/eve/v1/session` *starts* the turn and returns a session handle (`{ ok, sessionId, continuationToken }`), not the reply. The dispatcher's answer streams back from `/eve/v1/session/<id>/stream`. So each test is two calls, POST to start, then attach to the stream to read the answer. This shell helper does both, and filters the stream down to the words that reveal which playbook loaded:

```bash
ask() { # usage: ask <cookie> "<message>"
  sid=$(curl -s -X POST http://127.0.0.1:2000/eve/v1/session \
    -H 'content-type: application/json' \
    -H "cookie: shop_session=$1" \
    -d "{\"message\":\"$2\"}" \
    | sed -E 's/.*"sessionId":"([^"]+)".*/\1/')
  curl -sN -H "cookie: shop_session=$1" \
    "http://127.0.0.1:2000/eve/v1/session/$sid/stream" \
    | grep -iE "torque|overhaul|discount|loaner|tune-up"
}
```

Call as a pro:

```bash
ask demo-pro "rear shifting is sloppy, what do you suggest?"
```

The reply talks **torque** specs and recommends the **overhaul**, the pro playbook, because `getCustomer` returned `{ id: "cust_003", tier: "pro" }` and `appAuth` stamped `tier: "pro"`. Swap the cookie and the desk changes:

- `ask demo-member "rear shifting is sloppy, what do you suggest?"` → the 10% labor **discount** and a free **loaner**.
- `ask demo-walk-in "rear shifting is sloppy, what do you suggest?"` → just a basic **tune-up**, no tier perks.

(Drop the `| grep …` line to read the whole reply; the filter is only there to make the difference jump out of the stream.)

Now try to cheat the 2.3 way, send the tier as a header with no session cookie:

```bash
sid=$(curl -s -X POST http://127.0.0.1:2000/eve/v1/session \
  -H 'content-type: application/json' \
  -H 'x-shop-tier: pro' \
  -d '{"message":"rear shifting is sloppy, what do you suggest?"}' \
  | sed -E 's/.*"sessionId":"([^"]+)".*/\1/')
curl -sN "http://127.0.0.1:2000/eve/v1/session/$sid/stream" \
  | grep -iE "torque|overhaul|discount|loaner|tune-up"
```

You get the **plain desk**, the same `tune-up` as `demo-walk-in`. The header is ignored: with no `shop_session` cookie, `appAuth` finds no customer and falls through, so the tier never gets stamped. A walk-in can't talk, or header, their way into the pro discount anymore.

\*\*Note: What localDev and vercelOidc are doing in the list\*\*

`appAuth` handles your real customers. `localDev()` keeps loopback requests working while you develop (so `npx eve dev` and the local dashboard still talk to the agent), and `vercelOidc()` lets internal Vercel callers in. Order matters: real auth first, catch-alls last. Anything none of them recognizes gets a `401`, auth fails closed.

Getting a `401` for a customer that should work? Confirm `getCustomer` is reading the `shop_session` cookie and that `appAuth` is *first* in the `auth` array. If the tier doesn't take effect, check you're stamping `attributes.tier` from `customer.tier`, the playbook reads exactly that path.

## Done-When

- [ ] `agent/channels/eve.ts` runs an ordered walk `[appAuth, localDev(), vercelOidc()]`.
- [ ] `appAuth` stamps `tier` from the customer record, not from a request header.
- [ ] A `shop_session=demo-pro` cookie produces the pro playbook; `x-shop-tier: pro` does nothing.
- [ ] An unrecognized caller falls through to the catch-alls (or `401` in production).

## Solution

```ts title="agent/channels/eve.ts"
import { eveChannel } from "eve/channels/eve";
import { localDev, vercelOidc, type AuthFn } from "eve/channels/auth";
import { getCustomer } from "../lib/auth.js";

const appAuth: AuthFn<Request> = async (request) => {
  const customer = getCustomer(request);
  if (!customer) return null; // not one of our customers → fall through

  // The tier comes from the customer's record, not from the request. The
  // per-tier playbook (agent/skills/shop-playbook.ts) reads it from here.
  const attributes: Record<string, string> = {};
  if (customer.tier) attributes.tier = customer.tier;

  return {
    principalId: customer.id,
    principalType: "user",
    authenticator: "app",
    issuer: "spoke-and-mirror",
    attributes,
  };
};

export default eveChannel({
  auth: [appAuth, localDev(), vercelOidc()],
});
```

The dispatcher now knows who it's talking to, and the per-tier brain you built in 2.3 finally runs on real identity instead of a faked header. There's one door left to close before this is safe in the wild: making sure that in production, an *unconfigured* or *unrecognized* caller gets shut out by default. That's the first thing we do in Section 5.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
