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:
intentIdtenantIdadapterIdadapterTargettargetSystemproposedActiondecisionissuedAtexpiresAtnonce
Local Verification Flow#
- Parse token header and read
kid. - Fetch key set from
GET /tenants/:tenantId/authority-keys/public. - Select matching key by
kid. - 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#
| Failure | Cause | Action |
|---|
| Invalid signature | tampered token or wrong key | deny and log security event |
| Expired token | TTL exceeded | re-submit /intent to obtain a new token |
| Tenant mismatch | token replayed across boundary | deny, investigate actor context |
Unknown kid | key rotation | refresh 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
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#