OpenAgentIDDocumentation
Developer guides
OpenAgentIDDeveloper guide

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

AdapterContract
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 fieldPossessionDelegation
Protected typoaid-proof+jwtoaid-delegation+jwt
Protected algEdDSA/Ed25519 or RS256Same
Protected kidExact agent verification-method IDExact parent verification-method ID
iss / subSame exact agent DIDParent DID / child DID
audExact receiving audience stringSame
iat, nbf, exp, jtiRequired integer times and proof IDRequired
nonce, kindIssued 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?} and root: {kind: 'lifeid', did}.
  • Self-rooted: owner: null and root: {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.

On this page