Skip to content

Agent Authentication

This page explains how an agent authenticates as itself to SigID and receives an agent-scoped access token. For delegation (agents acting on behalf of users), see Delegation & Token Exchange.

What this page is for

  • Authenticating an autonomous agent using challenge-response
  • Setting up internal services with client credentials
  • Understanding the token shape and validation requirements for agent tokens
  • Troubleshooting common authentication failures

This page assumes you have already registered an agent. See Agent Registration if you need to create an agent first.

Agent identity

An agent identity consists of:

  • Agent ID – UUID identifying the agent
  • Keys – One or more registered signing keys (Ed25519, ES256, ES256K, BIP340)
  • Capabilities – Declared agent capabilities (e.g., web, mcp)
  • Key fingerprint – Hex-encoded SHA-256 hash of the public key

See Agent Registration to create an agent identity.

Before you start

You will need:

  • Agent record – A registered agent in your tenant
  • Active key – A registered, active key for challenge-response, or a confidential OAuth application for client credentials
  • Key fingerprint – Hex-encoded SHA-256 fingerprint of the public key
  • Allowed scopes – Scopes must be in the agent allowlist: openid, profile, email, offline_access, wallet:sign, vault:read
  • Issuer URL – Your SigID issuer URL (e.g., https://auth.example.com)

Choose an authentication method

Method Use case Key requirements Token type
Challenge-response Autonomous agents that hold their own signing keys Registered public key, key fingerprint Agent-scoped access token
Client credentials Internal services, legacy automation Confidential OAuth application with client_credentials grant enabled Agent-scoped access token
Token exchange Agent acting on behalf of a user User token + agent token Delegated token (see Delegation & Token Exchange)

Challenge-response authentication

Challenge-response is the recommended method for autonomous agents. The agent proves possession of a registered signing key by signing a challenge from SigID.

1. Request a challenge

Request

curl -sS "$SIGID_ISSUER_URL/auth/agent/challenge" \
  -X POST \
  -H "content-type: application/json" \
  -d '{
    "key_fingerprint": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "scope": "openid wallet:sign"
  }'

Response

{
  "challenge_id": "01234567-89ab-cdef-0123-456789abcdef",
  "nonce": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "timestamp": "2025-01-14T10:30:00Z",
  "expires_at": "2025-01-14T10:35:00Z",
  "audience": "https://auth.example.com",
  "client_id": "sigid-agent",
  "scope_hash": "a1b2c3d4...",
  "key_fingerprint": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "algorithm": "ed25519"
}

Field notes

  • challenge_id – Unique identifier for this challenge
  • nonce – Random 32-byte value (hex-encoded), used only once
  • timestamp / expires_at – Challenge validity window (typically 5 minutes)
  • audience – The SigID issuer URL
  • client_id – Always sigid-agent for agent challenges
  • scope_hash – SHA-256 hash of the sorted, deduplicated scopes (not the raw scopes)
  • key_fingerprint – The key that must sign this challenge
  • algorithm – Signature algorithm: ed25519, es256, es256k, or bip340

2. Build the canonical payload

SigID uses the SIGID-CHALLENGE-V1 format for signing. Canonicalize the challenge response as follows:

SIGID-CHALLENGE-V1
nonce:{nonce}
timestamp:{unix_seconds}
expires_at:{unix_seconds}
audience:{audience}
client_id:{client_id}
scope_hash:{scope_hash}
key_fingerprint:{key_fingerprint}
algorithm:{algorithm}

Example

SIGID-CHALLENGE-V1
nonce:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
timestamp:1736847800
expires_at:1736848100
audience:https://auth.example.com
client_id:sigid-agent
scope_hash:a1b2c3d4e5f6...
key_fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
algorithm:ed25519

Important

  • Use unix seconds (not ISO8601) for timestamp and expires_at
  • Escape backslashes and control characters in string fields
  • Sign the exact byte sequence (UTF-8)

3. Sign the payload

Sign the canonicalized payload using your registered private key. The signature must be base64-encoded.

Example (Ed25519)

import base64
from nacl.signing import SigningKey

canonical = b"SIGID-CHALLENGE-V1\nnonce:..."
signing_key = SigningKey(bytes.fromhex(private_key_hex))
signature = signing_key.sign(canonical)
signature_b64 = base64.b64encode(signature.signature).decode()

4. Verify and get token

Request

curl -sS "$SIGID_ISSUER_URL/auth/agent/verify" \
  -X POST \
  -H "content-type: application/json" \
  -d '{
    "challenge_id": "01234567-89ab-cdef-0123-456789abcdef",
    "signature": "base64-encoded-signature",
    "algorithm": "ed25519"
  }'

Success Response

{
  "access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6I...",
  "refresh_token": "def50200...}",
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "openid wallet:sign"
}

Error Responses

Error Cause
challenge_expired Challenge expired (timestamp exceeds expires_at)
challenge_used Nonce already consumed (one-time use)
key_revoked Key fingerprint is revoked
key_not_found Key fingerprint not registered
invalid_signature Signature verification failed
invalid_scope Requested scope not in allowlist

5. Use the token

Include the access token in the Authorization header:

curl -H "Authorization: Bearer $ACCESS_TOKEN" \
  https://api.example.com/protected

Client credentials for internal agents

For internal services or legacy automation, use the standard OAuth client_credentials grant.

Requirements

  • The OAuth application must be confidential (has a client_secret)
  • The application must allow client_credentials grant type
  • The audience comes from the application configuration, not the request

Request

curl -sS "$SIGID_ISSUER_URL/oauth/token" \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "content-type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=client_credentials"

Response

{
  "access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6I...",
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "openid"
}

Important

  • Client credentials tokens do not include a refresh token
  • The subject_type claim will be agent
  • Agent metadata may or may not be present (depends on application configuration)
  • Do not include audience in the request – it comes from the application config

Token shape and validation

Agent token claims

{
  "iss": "https://auth.example.com",
  "sub": "12345678-1234-1234-1234-123456789abc",
  "aud": "https://api.example.com",
  "exp": 1736848400,
  "iat": 1736848100,
  "jti": "01234567-89ab-cdef-0123-456789abcdef",
  "scope": "openid wallet:sign",
  "client_id": "sigid-agent",
  "subject_type": "agent",
  "agent": {
    "name": "My Agent",
    "capabilities": ["web", "mcp"]
  },
  "tenant_id": "tenant-uuid"
}

Validation requirements

Resource servers must validate:

Claim Validation
iss Must match your SigID issuer URL
aud Must match your resource server's audience
exp Must be in the future
sub Use with tenant_id as the principal key
subject_type Must be agent for agent-only APIs
scope Enforce required scopes for your API
tenant_id Prevent cross-tenant authorization
agent Present for challenge-response tokens, optional for client credentials

Critical

  • Always verify the signature, never decode JWT without verification
  • Never treat an agent token as a human user session
  • Always check subject_type when making authorization decisions
  • Never use email addresses as agent identifiers

See Verify Tokens for the complete validation contract.

Key lifecycle and failure cases

Key states

State Can authenticate? Notes
active ✅ Yes Normal state
deprecated ✅ Yes (with warning) Used during key rotation
revoked ❌ No Authentication fails with key_revoked

Key rotation

  1. Register a new key for the agent
  2. Update the agent to use the new key for authentication
  3. Deprecate or revoke the old key
  4. Deprecated keys return a WARNING header with sunset information

Failure cases

Scenario Error Recovery
Wrong key fingerprint key_not_found Use registered fingerprint
Revoked key key_revoked Register new key
Reused challenge challenge_used Request new challenge
Expired challenge challenge_expired Request new challenge
Invalid scope invalid_scope Use allowed scopes only

Common mistakes

Using scopes outside the allowlist

Wrong

"scope": "agent:read tools:execute"

Correct

"scope": "openid wallet:sign vault:read"

Including audience in client credentials request

Wrong

--data-urlencode "audience=https://api.example.com"

Correct

Audience comes from the OAuth application configuration.

Treating agent tokens as human sessions

Agent tokens have subject_type: "agent", not subject_type: "human". Always validate the subject_type claim before making authorization decisions.

Skipping signature verification

Never decode and trust a JWT without verifying its signature. Always use a proper JWT library or the Verify Tokens contract.

Reusing challenges

Challenges are single-use. Attempting to verify the same challenge twice will fail with challenge_used. Always request a fresh challenge for each authentication.

Next steps