Identity & Auth Guide
Single-line summary: Mentionable carries identity in two layers — transport-verified auth_method/verified and the extensible IdentityEvidence surface — with a 3-step trust chain (Connector verifies → Trusted Connector Issuer policy → Agent ACL).
See also: normalized-message-guide, building-a-connector, slack-identity-acl-cookbook, policy-part-guide, glossary
Spec source: docs/spec/identity-evidence-v0.1.md. Type source: packages/core/src/identity.ts.
Two Layers
┌────────────────────────────────────────────────────────┐
│ Layer 1: Transport verification │
│ → sender.auth_method (compact v0.1 view) │
│ → sender.verified │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ Layer 2: Extensible identity envelopes │
│ → sender.identities: IdentityEvidence[] │
│ (platform, delegated, OAuth, wallet, agent-signed) │
└────────────────────────────────────────────────────────┘
Layer 1 is “did the wire credential check out”. Layer 2 is “what verified principals are claiming this turn, possibly across trust boundaries”. Agents authorize on Layer 2.
sender.auth_method Values
| Value | Layer 1 verifier | Notes |
|---|---|---|
ap-http-signature | Fedify HTTP Signatures | The default for ActivityPub inbox delivery. |
ap-object-integrity-proof | Fedify object proof | Object-level signature over the activity body. |
a2a-jwt | A2A JWT validator | bearer-jwt agent-card auth scheme. |
a2a-oauth | A2A OAuth | oauth2 agent-card auth scheme. |
email-dkim | DKIM signature | First-class for inbound email. |
email-dmarc | DMARC alignment | Stronger inbound signal than raw DKIM. |
none | (no verification) | The adapter could not verify. verified: false. |
IdentityEvidence Shape
type IdentityEvidence = {
id?: string // jti / replay-detection key
subject: string // mailto:, acct:, slack:, eip155:, @agent@domain
issuer: string // who minted this evidence
method: string // open token; e.g. urn:mentionable:auth:slack-workspace-member:v0.1
assurance: 'platform' | 'domain' | 'address' | 'agent' | 'oauth' | 'wallet' | 'delegated' | string
audience: string | string[] // canonical recipient(s) this evidence is bound to
issued_at: string
not_before?: string
expires_at?: string
on_behalf_of?: string[] // delegation chain, immediate caller first
claims?: Record<string, unknown> // safe non-secret claims; profile under claims.profile
source?: { transport?: string; transport_module?: string; connector?: string; channel?: string }
proof: IdentityProof
}
Four Proof Types
proof.type | What it proves | Where it’s valid |
|---|---|---|
transport | Native transport verifier checked the proof; carries verified_by and optional key_id. | Inside the same runtime/transport boundary. NOT portable across origins unless re-wrapped. |
signed-attestation | Issuer-signed (Ed25519/JWS) over canonical evidence with proof omitted. Carries alg, kid, value, optional canonicalization. | Across origins, audience-bound, fresh, when receiver’s Trusted Connector Issuer policy accepts the issuer. |
bearer-token | Receiver verified an OAuth/bearer token against an issuer. Carries verified_by, optional token_type, key_id. | Trust scope of the verifying component. |
siwe | Sign-In with Ethereum proof verified by the adapter/connector. Carries verified_by, chain_id, address. | Same as bearer-token. |
The proof.type field is open — a (string & {}) escape hatch lets implementations carry namespaced future proof systems.
3-Step Trust Chain
A signed Connector attestation does NOT grant authority by itself. The chain is:
- Connector verifies the native platform. Slack signature, Discord interaction signature, OAuth ID token, SIWE, etc. The Connector knows what it just saw.
- Receiver’s Trusted Connector Issuer policy. A local policy entry says “this receiver trusts
mentionable-slack.exampleto issueurn:mentionable:auth:slack-workspace-member:v0.1evidence atassurance: platformfor subjects startingslack:”. Without this policy, even a cryptographically valid attestation is just a claim from an unknown party. - Per-agent ACL. The agent (or its system prompt) decides whether the verified subject is allowed to perform this specific action. See slack-identity-acl-cookbook.
Cryptographic validity is necessary but not sufficient. Agents that skip step 2 effectively trust anyone who signs anything.
Profile Claims
IdentityEvidence.claims.profile carries platform display facts (display_name, username, avatar, locale, timezone, provider, provider_subject, extensions). The receiver MAY project these into NormalizedMessage.sender.profile ONLY after the evidence’s signature, audience, freshness, and Trusted Connector Issuer policy all pass.
If verification fails for any reason, profile claims do not appear on sender.profile. This invariant is what makes sender.profile safe to render in an LLM Harness — anything visible there has been gated through the trust chain.
Well-Known Methods
| Method URI | Issuer | Subject shape |
|---|---|---|
email-dkim | (transport) | mailto:user@domain |
email-dmarc | (transport) | mailto:user@domain |
ap-http-signature | (transport) | acct:user@host or actor IRI |
ap-object-integrity-proof | (transport) | actor IRI |
a2a-jwt / a2a-oauth | (transport) | issuer-defined subject |
urn:mentionable:auth:slack-workspace-member:v0.1 | Slack Connector instance | slack:T<workspace>/U<user> |
urn:mentionable:auth:agent-self-sign:v0.1 | the agent itself (AgentCard signing_key) | @agent@domain |
urn:mentionable:auth:oauth:v0.1 | OAuth issuer URL | issuer-defined subject |
urn:mentionable:auth:siwe:v0.1 | (Connector or transport) | eip155:<chainId>:<address> |
The method registry is open. Anyone can mint a new method URI; receivers control whether they trust it via Trusted Connector Issuer policy.
Security Rules
- Audience-bound.
audienceMUST equal the receiver’s canonical address (@<local>@<domain>) before processing. Wildcards are not allowed in v0.1. - Time-bound. Always check
not_beforeandexpires_at. The default forwarded freshness ismaxAge: 600s,maxTtl: 600s,clockSkew: 60s,requireExpiresAt: true. - Local policy before remote metadata fetch. A receiver MUST consult its Trusted Connector Issuer policy BEFORE fetching the issuer’s Connector Card or JWKS. Otherwise an attacker can use you to probe arbitrary URLs.
- HTTPS only. Reject Connector Cards served over
http, loopback, or private hostnames in production. - No raw secrets in
claims. No OAuth tokens, no SIWE raw signatures (unless explicit opt-in), no platform API tokens. Only safe public-ish facts. - Replay detection. When an upstream platform gives you a stable replay id, persist it in
idand reject duplicates.
Chained Agent Calls — on_behalf_of
When agent A calls agent B on behalf of user U, the upstream user authority travels in on_behalf_of:
{
subject: '@agent-a@example.com',
on_behalf_of: ['mailto:u@example.com'], // immediate caller first
...
}
A multi-hop chain has multiple entries; the immediate caller is at index 0. Agent B’s authorization layer decides whether the chain (subject + delegation) collectively authorizes the action. This composes with [[multi-agent-composition|AgentChain hop tracking]] but is conceptually independent — AgentChain bounds chain depth, on_behalf_of carries authority.
Identity Policy
IdentityPolicy lets receivers express purpose-scoped rules:
type IdentityPolicy = {
default?: 'deny-by-default' | 'accept-any-valid-evidence'
accepts?: Array<{
issuers?: string[]
methods?: string[]
subjects?: string[]
assurance?: IdentityAssurance[]
purposes?: IdentityPurpose[] // basic-use | terms-invocation | account-linking | payment | delegation | destructive-action | sensitive-data
}>
step_up_required_for?: IdentityPurpose[]
}
Default to deny-by-default for any non-trivial purpose. Use step_up_required_for: ['payment', 'destructive-action'] to require a fresh PolicyPart-driven step-up before risky operations.