---
summary: Authenticate SigID agents with challenge-response, client credentials, or token exchange and validate agent-scoped access tokens.
tags:
  - agents
  - authentication
  - challenge response
  - client credentials
  - token validation
categories:
  - AI Agents
---

# Agent Authentication

<!-- agent:page
You are a coding agent implementing SigID agent authentication by following this reference.
First collect: issuer URL, tenant or workspace ID, agent ID, key fingerprint, public-key algorithm, allowed scopes, target API audience, and whether the caller uses challenge-response, client credentials, or RFC 8693 token exchange.
For challenge-response, request a tenant-scoped single-use challenge, sign the exact canonical payload with the registered private key, submit verification, and validate returned access tokens by signature, issuer, audience, expiry, tenant, scope, and subject_type = agent.
For client credentials, use only a confidential OAuth application with client_credentials explicitly allowed; do not accept a request-supplied audience.
Never treat an agent token as a human session, never skip signature, audience, or tenant checks, and never log private keys, raw tokens, or signatures.
Verify failures for expired or reused challenges, wrong fingerprints, revoked keys, invalid signatures, wrong audience, wrong tenant, and missing scopes.
-->

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](delegation.md).

## 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](registration.md) 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](registration.md) 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](delegation.md)) |

## 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**

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

**Response**

```json
{
  "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)**

```python
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**

```bash
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**

```json
{
  "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:

```bash
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**

```bash
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**

```json
{
  "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

```json
{
  "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](../developers/verify-tokens.md) 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**

```bash
"scope": "agent:read tools:execute"
```

✅ **Correct**

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

### Including audience in client credentials request

❌ **Wrong**

```bash
--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](../developers/verify-tokens.md) 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

- [Agent Registration](registration.md) – Create and configure agents
- [Delegation & Token Exchange](delegation.md) – Agents acting on behalf of users
- [Agent Wallets](wallets.md) – Wallet policies and signing controls
- [Verify Tokens](../developers/verify-tokens.md) - Backend validation contract
