Skip to content

guides

Intended Documentation

Verify Decision Tokens

Verify RS256 authority decision tokens locally and with the verification gateway.

Verify Authority Tokens#

Tokens returned in authorityDecisionToken are signed with RS256 and scoped to tenant, adapter, and action claims.

Token Claims (Runtime)#

Typical claim set includes:

  • intentId
  • tenantId
  • adapterId
  • adapterTarget
  • targetSystem
  • proposedAction
  • decision
  • issuedAt
  • expiresAt
  • nonce

Local Verification Flow#

  1. Parse token header and read kid.
  2. Fetch key set from GET /tenants/:tenantId/authority-keys/public.
  3. Select matching key by kid.
  4. Verify JWT signature and required claim constraints.

TypeScript Example#

typescript
import { createPublicKey } from "node:crypto";
import { jwtVerify, decodeProtectedHeader } from "jose";

const token = process.env.AUTHORITY_TOKEN!;
const tenantId = "tenant_acme_prod";

const header = decodeProtectedHeader(token);
if (!header.kid) throw new Error("Missing kid");

const keysResp = await fetch(`https://api.intended.so/tenants/${tenantId}/authority-keys/public`, {
  headers: { Authorization: `Bearer ${process.env.API_KEY}` },
});
const keysJson = await keysResp.json();
const key = keysJson.keys.find((k: any) => k.kid === header.kid);
if (!key) throw new Error("No matching key for kid");

const publicKey = createPublicKey(key.publicKeyPem);
const { payload } = await jwtVerify(token, publicKey, {
  algorithms: ["RS256"],
});

if (payload.tenantId !== tenantId) throw new Error("Tenant mismatch");
if (payload.decision !== "APPROVED") throw new Error("Token is not approved");

Gateway Verification Flow#

Use POST /verify/token when you want centralized verification.

bash
curl -X POST https://api.intended.so/verify/token \
  -H "Authorization: Bearer $API_KEY" \
  -H "x-tenant-id: tenant_acme_prod" \
  -H "Content-Type: application/json" \
  -d '{
    "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIn0...",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----...",
    "expectedKid": "key-1",
    "expectedTenantId": "tenant_acme_prod",
    "expectedAdapterId": "github-actions-adapter",
    "maxTokenTtlSeconds": 300,
    "clockSkewSeconds": 30
  }'

Failure Handling#

FailureCauseAction
Invalid signaturetampered token or wrong keydeny and log security event
Expired tokenTTL exceededre-submit /intent to obtain a new token
Tenant mismatchtoken replayed across boundarydeny, investigate actor context
Unknown kidkey rotationrefresh keys and retry

On-Robot Verification (Physical AI)#

For physical-AI runtimes, verify the authority token at the robot before actuation lands. Cloud-side approval isn't enough — the robot must prove offline that this specific action was authorized by Intended for this specific tenant.

Python (ROS2)#

python
from intended_ros2.token_verifier import (
    AuthorityTokenVerifier,
    VerifierConfig,
)

verifier = AuthorityTokenVerifier(
    VerifierConfig(
        jwks_url="https://api.intended.so/.well-known/jwks.json",
        expected_tenant_id="tenant-a",
        expected_audience="intended-edge",
        expected_issuer="https://api.intended.so",
        # Hardware-attested mode: only honour tokens whose cnf.jkt
        # matches one of the robot's TPM/SE-OS attestation thumbprints.
        expected_attestation_keys=("robot-tpm-thumbprint-a",),
    )
)

result = verifier.verify(token, expected_action="navigate_to_pose")
if not result.allowed:
    # refuse the actuation; log result.reason
    return
# proceed with motion

Go#

go
import "github.com/intended-so/intended-go"

v, _ := meritt.NewAuthorityTokenVerifier(meritt.VerifierConfig{
    JWKSURL:                 "https://api.intended.so/.well-known/jwks.json",
    ExpectedTenantID:        "tenant-a",
    ExpectedAudience:        "intended-edge",
    ExpectedIssuer:          "https://api.intended.so",
    ExpectedAttestationKeys: []string{"robot-tpm-thumbprint-a"},
})
result := v.Verify(token, "navigate_to_pose", "")
if !result.Allowed {
    // refuse
}

Both verifiers enforce: signature, expiry, audience, issuer, tenant scoping, action binding, intent-id binding (optional), nonce replay protection, and (when configured) hardware attestation via the cnf.jkt claim. Reason codes: TOKEN_EXPIRED, TOKEN_TENANT_MISMATCH, TOKEN_ACTION_MISMATCH, TOKEN_AUDIENCE_MISMATCH, TOKEN_ISSUER_MISMATCH, TOKEN_NONCE_REPLAY, TOKEN_NONCE_MISSING, TOKEN_CNF_MISSING, TOKEN_ATTESTATION_MISMATCH.

Next Steps#

Verify Decision Tokens | Intended