Agent token: how to use it
This page is written for an agent rather than for you: the note shown when a token is issued hands it the secret and this address, and everything it needs afterwards is below. <APP_URL> is the application it should work in.
An agent works inside the applications of the estate, not inside identity itself. It reaches an operator backoffice (/bo) only if you hold the platform role and delegate it explicitly β and it loses that access the moment the role is taken away from you.
In English on purpose: this part is addressed to a machine.
# Using a hodor agent token
A hodor agent token is a static secret starting with "hodor_agent_". A person issues
it on their profile at https://hodor.dev.picaz.ru/user and hands it to their agent; it lets the
agent act as that person, with the company and access levels they picked, until it
expires or they revoke it. Keep it in a config file or an environment variable β
never in a chat message and never in a commit.
## 1. Exchange the secret for an access token
curl -sS -X POST https://hodor.dev.picaz.ru/api/v1/agent/token \
-H 'content-type: application/json' \
-d '{"token":"<AGENT_TOKEN>"}'
{"access_token":"<JWT>","token_type":"Bearer","expires_in":900,"company_id":"...","role":"..."}
The JWT lives about 15 minutes and there is no refresh token: when it expires, call this
endpoint again with the same secret. The secret itself does not rotate.
## 2. Calling an application's API
curl -sS <APP_URL>/api/v1/... -H "Authorization: Bearer <JWT>"
Identity's own API (https://hodor.dev.picaz.ru/api/v1) refuses this token on purpose β 403 with code
"client_token". The delegated line is for the applications of the estate, not for hodor.
## 3. Driving an application's UI in a browser
An estate application keeps its browser session in a cookie whose value IS the JWT; there
is no server-side session store. Do not replay the sign-in ceremony β set the cookie and
navigate straight to the page.
name __Host-bo_session over https
bo_session over plain http (local dev)
value <JWT>
path /
secure true (the __Host- prefix requires it)
httpOnly true
sameSite Lax
Playwright:
const r = await fetch('https://hodor.dev.picaz.ru/api/v1/agent/token', {
method: 'POST', headers: {'content-type': 'application/json'},
body: JSON.stringify({ token: process.env.HODOR_AGENT_TOKEN }),
}).then(r => r.json());
await context.addCookies([{
name: '__Host-bo_session', value: r.access_token,
domain: '<APP_HOST>', path: '/', secure: true, httpOnly: true, sameSite: 'Lax',
}]);
await page.goto('https://<APP_HOST>/the/page/you/need');
Name the host, not the url: Playwright refuses a cookie that carries "url" together
with "path" ("Cookie should have either url or path"), and the __Host- prefix needs
the explicit path and secure anyway.
Set the cookie again after ~15 minutes: an expired JWT is not refreshed by the
application, it just sends you to /signin.
## 4. Walking the pages as the person, identity's own included
Planting the cookie works per application. To browse the way the person does β
including identity's own cabinet, which runs on a session and not on this JWT β trade
the secret for a GUEST SESSION and let the normal sign-in flows do the rest:
await page.request.post('https://hodor.dev.picaz.ru/agents/signin', {
form: { csrf: '<from the page>', token: process.env.HODOR_AGENT_TOKEN },
});
await page.goto('https://hodor.dev.picaz.ru/user'); // identity's cabinet
await page.goto('<APP_URL>/signin'); // any estate app: the ceremony just works
The guest session lasts at most an hour and never longer than the token itself, and it
is bounded twice over. It refuses everything that changes how the user signs in β
password, second factor, linked users, issuing or rotating agent tokens β and it
carries exactly your token's grant: company management needs "admin" in it, an operator
backoffice needs "platform:admin", and a ceremony started from it hands the application
a token narrowed to the same scopes (and no refresh token). Nothing about looking at the
pages makes your delegation wider than it was.
## What your token can do β read it, do not guess
The JWT payload (the middle dot-separated part, base64url) says what you were given:
"scope" lists the access levels, "company"/"company_role" the tenant, "azp" is
"agent:<name>". Read it once instead of discovering the answer as a 403.
## Boundaries
- Operator backoffices (the /bo of any application) admit on the scope platform:admin.
Your token carries it only if the person who issued it holds the platform role and
chose to delegate it; without that scope those screens are closed and no retry opens
them. The scope is also re-checked at every exchange β if the person is demoted, the
next JWT comes back without it.
- Another company: the token names one company (or none) and cannot switch.
- Identity's own portal (https://hodor.dev.picaz.ru/user) and its /bo do not read this JWT at all β
they run on a session cookie. Section 4 is the way in, and what it opens is the
person's pages, never their credentials.
A page that sends you to /signin means, in order of likelihood: no cookie on that host,
an expired JWT, or a scope this token does not carry. Retrying changes none of the three
β re-exchange once, and if that does not help, ask the person who gave you the token.