Portable identity verification
Verify key possession, current root authority, signed delegation, revocation and atomic replay at a service boundary.
IdentityVerifier in SDK 0.7.2 verifies a signed possession proof against the receiving service's current authority. Use local workspaces for the complete single-host implementation, or supply the following trusted adapters for a distributed service.
Authorities the service supplies
| Adapter | Contract |
|---|---|
resolveDid(did) | Current exact DID and public verification methods {id, publicKeyJwk, purposes} from an authoritative resolver; null denies. |
authorizeOwner(rootDid) | Current real owner {issuer, subject, organizationId?} for an owned root, or null; never echo proof claims. |
checkRevocation({did, issuerDid?, credentialId?}) | Current DID and credential status {revoked, checkedAt} from the authoritative store. |
replay.consume({issuer, tokenId, audience, nonce, expiresAt}) | Atomically validate/consume the service-issued challenge and unique proof ID in durable storage; success exactly once. |
resolveDelegations(did, audience) | Optional for verify, required by resolve; current signed root-first delegation JWTs from the configured authority. |
authorizeSelfRoot(did, {audience}) | Optional current self-root policy grant for the exact DID/audience, or null. |
The first four are mandatory. Public keys in an incoming JWT, remote jku URLs, a caller-declared owner or a decoded subject never configure authority. Authoritative key relationships explicitly permit authentication for possession and capabilityDelegation for delegation. An assertion-only key cannot authenticate a caller merely because it verifies a signature.
A self-root grant contains did, audience, authorizationId, revision, issuedAt, notBefore, expiresAt and checkedAt in Unix seconds. Its key, freshness, lifetime and authorization-ID revocation are checked. A self-root caller needs explicit current approval; an empty chain alone does not establish it. Owned roots use the configured real person or organization authority. Failure on an owned path never falls back to a self root.
Verify the actual proof
The transport is {token: string, delegations: readonly string[]}. Delegations are ordered root-to-agent; a direct approved self root has an empty array.
import {
IdentityVerifier,
type IdentityVerifierConfig,
type AgentIdentityProof,
} from '@openagentid/sdk/identity-verification';
export async function authenticateAgent(
authorities: IdentityVerifierConfig,
proof: AgentIdentityProof,
audience: string,
issuedNonce: string,
) {
const verifier = new IdentityVerifier(authorities);
return verifier.verify(proof, { audience, nonce: issuedNonce });
}This integration function requires real service authorities and an already issued nonce. It does not create a registry or substitute fabricated callback results.
| JWT field | Possession | Delegation |
|---|---|---|
Protected typ | oaid-proof+jwt | oaid-delegation+jwt |
Protected alg | EdDSA/Ed25519 or RS256 | Same |
Protected kid | Exact agent verification-method ID | Exact parent verification-method ID |
iss / sub | Same exact agent DID | Parent DID / child DID |
aud | Exact receiving audience string | Same |
iat, nbf, exp, jti | Required integer times and proof ID | Required |
nonce, kind | Issued nonce and kind: 'agent' | Not possession evidence |
Default maximum proof age is 300 seconds, delegation lifetime 86,400 seconds, depth 16 links, revocation freshness 60 seconds and clock tolerance zero. Lifetime cannot exceed the applicable credentials/root authorization. did:oas and readable legacy did:l1fe values stay literal throughout verification; rewriting a signed identifier creates a different principal.
Result and current lineage
A verified result includes did, root-first lineage, expiresAt, and one root mode:
- Owned:
owner: {issuer, subject, organizationId?}androot: {kind: 'lifeid', did}. - Self-rooted:
owner: nullandroot: {kind: 'self', did, authorizationId, revision}.
Historical owned results without root remain readable. Ownerless results require the explicit self discriminator. Neither mode creates application permissions, an organization, a payment account or provider credentials.
verifyLineage(did, delegations, {audience}) checks current signatures, root policy and revocation without proving the leaf's possession. resolve(did, {audience}) loads that chain from resolveDelegations and performs the same current checks. Use these after independently verifying evidence, such as an OATS credential; do not replay an old possession proof as a current status lookup.
Host a Fetch verification API
import { IdentityVerifier, type IdentityVerifierConfig } from '@openagentid/sdk/identity-verification';
import { createIdentityVerificationHandler } from '@openagentid/sdk/verification-api';
export function verificationEndpoint(
authorities: IdentityVerifierConfig,
audience: string,
resolveNonce: (request: Request) => Promise<string>,
) {
return createIdentityVerificationHandler(new IdentityVerifier(authorities), {
audience, resolveNonce,
});
}Mount the returned Fetch handler at your chosen HTTPS POST route. Requests contain only {proof} with Content-Type: application/json. The server fixes the audience and authorities. resolveNonce(request) must look up its previously issued challenge and check audience, session/tenant binding and expiry; merely echoing X-OpenAgent-Nonce is insufficient. replay.consume performs the final atomic consumption.
Responses are no-store: 200 returns {identity}, 400/413/415 refuse malformed/oversized/media input, 401 refuses invalid or revoked identity/replay, and 503 refuses unavailable authority. The default body limit is 128 KiB. The handler does not mount a challenge-issuance route; the host owns that transport and its storage.
A configured client calls verifyIdentityRemote(endpoint, proof, {nonce, fetch?, signal?, timeoutMs?}) from the same module, or oaid verify --endpoint HTTPS_URL --proof FILE --nonce ISSUED_NONCE. The client requires HTTPS, omits ambient cookies, refuses redirects, bounds the result and checks DID/nonce/expiry/lineage correlation. The configured service remains the trusted verification authority; parsing its verdict is not independent signature verification.