Agent Sign-In lets an AI agent sign in to your application the way a person does — through the browser-based OAuth/OIDC authorization code flow — with an identity its owner controls. Your app adds two config values. The agent enrolls once. After that, the agent signs in by itself, and the owner can revoke it with one click.
Agents here sign into your app. If you instead need to verify agents that call your API directly, use machine identity — the two products solve different problems and are not interchangeable. For the complete picture, see AgentSec.
How it works
Five things, no more:
| Thing | What it is |
|---|---|
| Agent identity | A synthetic address like [email protected] on a tenant domain you have verified, owned by a directory user |
| Sign-in key | The public half of a P-256 keypair generated in the agent's browser. The private half never leaves that browser |
| Open client | Your app's client_id is simply its https origin. No registration, no secret, no client row |
| Remembered approval | The first sign-in asks the agent (a human approving on its behalf) to trust your app; approval is remembered and slides 180 days |
| Honest revocation | The owner can suspend or revoke the agent, or revoke a single key, at any time — with exactly the effects documented below |
Your app: two config values
Point any OAuth/OIDC library at your Authdog environment:
client_id— your app's https origin, e.g.https://app.example.com. That's it. Nothing to register.redirect_uri— any https URL on that origin (or a subdomain), e.g.https://app.example.com/callback.
Two rules are enforced server-side:
- PKCE with
code_challenge_method=S256is mandatory. Open clients have no secret; PKCE is the transaction integrity. - Scopes are capped at
openid email profile. Anything outside that ceiling is rejected withinvalid_scope.
Start the sign-in
GET {issuer}/oidc/{environmentId}/authorize?
client_id=https://app.example.com
&redirect_uri=https://app.example.com/callback
&response_type=code
&scope=openid email profile
&state=...
&nonce=...
&code_challenge=...
&code_challenge_method=S256Read authorization_endpoint and token_endpoint from the environment's
OIDC discovery document. On success you are redirected to your
redirect_uri with a single-use code (valid 60 seconds) and your state.
Exchange the code
curl -X POST "$TOKEN_ENDPOINT" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "client_id=https://app.example.com" \
--data-urlencode "code=$CODE" \
--data-urlencode "redirect_uri=https://app.example.com/callback" \
--data-urlencode "code_verifier=$VERIFIER"Response:
{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 600,
"id_token": "…",
"scope": "openid email profile"
}There is no refresh token — by design. A refresh token is a standing
credential; agents re-run the authorization code flow instead, which is
zero-interaction once the approval is remembered. When the access token
expires, run authorize again.
Call userinfo
curl "$USERINFO_ENDPOINT" -H "Authorization: Bearer $ACCESS_TOKEN"The issuer re-checks the agent's status and the signing key's status on
every userinfo call. A suspended agent, a revoked agent, or a revoked
key makes the token stop working immediately with invalid_token.
The agent side
Enroll once
The agent's owner creates the agent identity in the console and opens the generated enrollment link in the agent's browser (the browser profile the agent actually runs in). That page offers two storage options for the same key model:
- Generate key in this browser — generates an ECDSA P-256 keypair
locally, marks the private half non-exportable and never transmitted,
keeps it in the browser, and sends only the public half plus a key id
(
kid) to Authdog. - Generate key file (headless agents) — for agents that sign in from
their own runtime (no browser). The private half is written to an
agent-key.pemfile only you control; move it to the agent's runtime. Store it like a password. The key id is shown for the sign-in command.
Keys live 30 days from activation. Enroll a new key before the old one
expires to avoid a sign-in gap. Up to 5 active keys per agent are kept —
rotation means: enroll the new key, confirm sign-in works, revoke the old
one. The agent's subject (sub) never changes during rotation.
Sign in
When your app redirects to authorize, the issuer opens a waiting page in
the agent's browser:
Headless agents — the page shows a
RUN THIS WITH YOUR AGENTcommand. The agent's runtime signs the page's one-time auth token with its key file and POSTs the proof:AUTHDOG_KEY=./agent-key.pem \ curl -X POST "https://<issuer>/oidc/<environmentId>/agents/wait" \ --data-urlencode "auth_token=<one-time token from the page>" \ --data-urlencode "kid=$AUTHDOG_KID" \ --data-urlencode "signature=$(printf %s "$AUTH_TOKEN" | openssl pkeyutl -sign -inkey $AUTHDOG_KEY | openssl base64 -A | tr '/+' '_-' | tr -d '=')"The page shows what your app will receive, polls, and the moment the proof lands the browser is sent back to your
redirect_uriwith the code. The transaction expires in 5 minutes; the command is safe to re-run against a fresh page. Proving also remembers the approval for your app, so later headless sign-ins stay one command.Browser agents — "Sign in manually" opens the browser-key path: the agent proves possession of its stored key by signing a short-lived challenge, gets a 30-day session cookie, and is sent straight back to your app. The first sign-in for a given app also shows a one-time approval page.
Both paths are automatic on every later sign-in — the whole loop is zero-interaction from your app's point of view.
Tokens and claims
The id_token is a signed JWT with a 10-minute lifetime (600 seconds),
signed with the environment's active key and pinned by kid. aud is your
exact client_id (the origin), iss is the environment issuer, and the
nonce you sent is echoed back.
| Claim | Scope | Value |
|---|---|---|
sub |
always | The agent's 43-character opaque subject. Stable while the agent exists; a deleted and recreated agent gets a new sub |
actor_type |
always | The literal string agent |
email |
email |
The agent's synthetic address, e.g. [email protected] |
email_verified |
email |
true — the address is synthetic on a domain the tenant verified |
name |
profile |
The agent's display name |
preferred_username |
profile |
Derived deterministically from the address |
Claims are present or absent, never null: if a scope was not granted,
its claims are simply omitted from the token and the userinfo response.
There are no owner_* claims in this release.
Lifetimes
Every number here is pinned by the issuer's test suite.
| Value | Lifetime |
|---|---|
| Waiting-page auth token | 5 minutes, one proof |
| Authorization code | 60 seconds, single use |
| Access token / id_token | 10 minutes (600 seconds) |
| Sign-in challenge | 5 minutes |
| Agent session cookie | 30 days |
| Sign-in key | 30 days from activation |
| Remembered approval | 180 days, sliding (refreshed at each sign-in) |
Revocation semantics
Revocation is honest — each action does exactly this and nothing else:
| Action | Immediate effect | Tokens already issued | Sessions your app created |
|---|---|---|---|
| Revoke a sign-in key | That key can no longer prove sign-in; new sign-ins with it stop | Stop working within 10 minutes (userinfo re-checks the key on every call) | Not affected — they belong to your app |
| Suspend the agent | All sign-ins blocked | Stop working within 10 minutes | Not affected |
| Revoke the agent | Same as suspend, and the agent is marked revoked permanently | Stop working within 10 minutes | Not affected |
| Delete the agent | Everything above, plus the identity is gone | Stop working | Not affected |
Third-party app sessions created from an agent sign-in are the app's sessions — Authdog revokes the Authdog side (sign-ins and tokens), never your app's session records.
Security notes
client_idvalues that are not https origins (http, localhost, URLs with paths) are rejected before anything else happens.- Redirect URIs must be https and on the client origin's host or a subdomain of it.
- Codes are stored hashed and are single-use; replaying one returns
invalid_grant. - The agent endpoints are rate-limited per environment and per IP alongside the rest of the OIDC surface.
- Approving an app is remembered per scope set and per redirect URI: if your app later requests different scopes or a different callback, the approval page is shown again.