Human Key Issuance — Garden-Scoped Permissions
What this page is
Track C — C2 adds a permission ceiling to the OIDC exchange: the scoped key issued to a human is capped by that person's garden membership role. Node operators no longer manage permission grants manually — they flow from the curator roster.
This page covers the garden membership scoping layer. For the full OIDC bridge configuration (env vars, IdP setup, token exchange flow), see OIDC / SSO Integration.
How garden membership gates key permissions
When a human calls POST /v1/auth/oidc/exchange, the node:
- Validates the
id_tokenand derivesentity_uri = oidc:<sub>. - Looks up the caller's rows in
garden_membersto determine their permission ceiling:adminorwriterin any garden → ceiling iswrite;readeronly or no membership → ceiling isread. - Intersects the caller's requested
permissionswith that ceiling. - Returns the key scoped to the intersection.
Requested permissions: ["read", "write"]
Garden role: reader only
Issued permissions: ["read"] ← write silently dropped
Requested permissions: ["read", "write"]
Garden role: writer in at least one garden
Issued permissions: ["read", "write"] ← full grant
The federate permission is never grantable via OIDC exchange.
Garden membership or not. Operators who need federate
must provision static keys out-of-band.
Exchange response includes granted permissions
The ExchangeResponse now includes a permissions field:
{
"api_key": "stgm_...",
"entity_uri": "oidc:alice@example.com",
"permissions": ["read", "write"],
"expires_at": "2026-05-03T17:00:00Z"
}
Clients should read permissions on each exchange to know what access
they were granted — do not cache the previous value across token
rotations.
Setting up garden membership for human users
Grant a human OIDC principal membership in a garden before they exchange a token:
# Add alice as a writer in the team garden
curl -s -X POST http://localhost:8000/v1/gardens/team/members \
-H 'Authorization: Bearer <admin-key>' \
-H 'Content-Type: application/json' \
-d '{
"entity_uri": "oidc:alice@example.com",
"role": "writer"
}'
Role values:
adminwriterreaderA single admin or writer role in any garden grants the full
ceiling — it is not per-garden.
Minimal example · OIDC exchange with garden membership
# 1. Obtain an id_token from your IdP (exact command is IdP-specific)
ID_TOKEN="<id_token from Google / GitHub / etc.>"
# 2. Exchange for a scoped Stigmem key
RESPONSE=$(curl -s -X POST http://localhost:8000/v1/auth/oidc/exchange \
-H 'Content-Type: application/json' \
-d "{\"id_token\": \"$ID_TOKEN\"}")
API_KEY=$(echo $RESPONSE | jq -r .api_key)
PERMS=$(echo $RESPONSE | jq -r '.permissions | join(",")')
echo "Issued permissions: $PERMS"
# 3. Use the key
curl -H "Authorization: Bearer $API_KEY" http://localhost:8000/v1/facts
Security notes
Membership checked at exchange time
Not at API request time. Changing a member's role in the garden does not immediately revoke an active key; the new ceiling applies on the next exchange.
Single-session-per-principal
Every successful exchange revokes all prior OIDC keys for the same sub. Revoking garden membership and waiting for the current token to expire (or exchanging immediately to pick up the reduced ceiling) is the recommended offboarding flow.
No-membership default
Users who are not in any garden receive read-only access. Safe default for pilot users or self-hosted nodes without curated gardens.