Authentication & signing

Four sign-in paths — email/password, wallet signature, provider exchange, API key. Same JWT works against every YF endpoint.

Identity and signing. Every party in the system — your users, the other side of a deal, agents acting on behalf of a group — has an account backed by signing keys. Logging in returns a JWT; the same JWT works against every YF endpoint and is what the platform uses to authorise on-chain operations on behalf of the caller.

There are four sign-in paths. Browser clients use one of the first three; backend services (operators, app backends, batch jobs) use the fourth.

#
1
Path
Email + password
Use it when
Easiest for humans. Standard /auth/login.
#
2
Path
Wallet signature
Use it when
The party already holds a wallet — MetaMask, Ledger, etc. EIP-191 sign-in.
#
3
Path
Provider exchange
Use it when
Federated identity — Averer, MetaMask via Dynamic, WebAuthn, email-link.
#
4
Path
API key
Use it when
Server-side code. Issue once, exchange for short-lived JWT at boot. Don't store passwords in env vars.

All four return the same response shape — { token, refresh_token, expires_in, user } — and the resulting JWT is interchangeable across auth, payments, agents, and the federation gateway.

1. Email + password

LOGIN=$(curl -s -X POST $YF_AUTH/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "your-password"
  }')

export TOKEN=$(echo "$LOGIN" | jq -r .token)
export REFRESH=$(echo "$LOGIN" | jq -r .refresh_token)
export USER_ID=$(echo "$LOGIN" | jq -r .user.id)

Response:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "...",
  "token_type": "Bearer",
  "expires_in": 900,
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "user@example.com",
    "role": "User"
  }
}

2. Wallet signature

For wallet-led users who hold their own keys. Hit the nonce endpoint, sign the nonce off-chain (EIP-191 personal_sign), exchange.

# 1. Fetch a fresh nonce
NONCE=$(curl -s $YF_AUTH/auth/signature/nonce | jq -r .nonce)

# 2. Wallet signs $NONCE off-chain (returns signature)

# 3. Exchange
curl -s -X POST $YF_AUTH/auth/signature/signin \
  -H "Content-Type: application/json" \
  -d "{
    \"public_key\": \"0x04…\",
    \"signature\": \"0x…\",
    \"message\": \"$NONCE\",
    \"nonce\": \"$NONCE\"
  }"

3. Provider exchange

For federated identity providers configured in this deployment. Discover what's available first, then exchange.

# Discover providers (Averer, MetaMask, Email, WebAuthn, Dynamic, …)
curl $YF_AUTH/auth/providers/config

# Per-provider exchange (shape varies — see /docs/api/auth)
curl -X POST $YF_AUTH/auth/metamask/exchange \
  -H "Content-Type: application/json" \
  -d '{
    "address": "0x…",
    "signature": "0x…",
    "message": "<contains nonce>",
    "chain_id": 1
  }'

4. API key (the backend pattern)

For non-browser callers — your server-side code, the yieldfabric-operator pattern, batch jobs, anything that doesn't log a human in at boot. Don't put an email + password in env vars; mint an API key instead. Two steps, then you're done:

Step A — Issue the key once, by hand:

# As that service account user (one-time login with any flow above)
curl -X POST $YF_AUTH/auth/api-key/generate \
  -H "Authorization: Bearer $ONE_TIME_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "service_name": "my-app-backend",
    "description": "Backend that submits obligations on behalf of admin"
  }'
# → { "api_key": "yf_api_…", "service_name": "…", … }

The raw yf_api_… value is returned only at creation time — the server only keeps a hash. Save it; store it as API_KEY in your service's env / secret manager.

Step B — At boot (and on token refresh), exchange the key for a JWT:

curl -X POST $YF_AUTH/auth/api-key \
  -H "Content-Type: application/json" \
  -d "{\"api_key\":\"$API_KEY\"}"
# → same shape as /auth/login

Why this matters: API keys are individually revocable (POST /auth/api-keys/{key_id}/revoke) and auditable (GET /auth/api-keys) without rotating the underlying user's password. The tncshell reference impl uses this pattern.

Token lifetimes

Token
Access token
Lifetime
15 min
Notes
The token field of the login response. Use as Authorization: Bearer.
Token
Refresh token
Lifetime
30 days
Notes
Single-use. Each refresh returns a new refresh_token — store the new one atomically. Using the same refresh twice returns 401.

Refreshing

NEW=$(curl -s -X POST $YF_AUTH/auth/refresh \
  -H "Content-Type: application/json" \
  -d "{\"refresh_token\":\"$REFRESH\"}")
export TOKEN=$(echo "$NEW" | jq -r .token)
export REFRESH=$(echo "$NEW" | jq -r .refresh_token)

Resolve the caller from a JWT

curl $YF_AUTH/protected/jwt \
  -H "Authorization: Bearer $TOKEN"
# → { "user_id": "...", "default_wallet_id": "...", "account_address": "0x...", "role": "...", ... }

curl $YF_AUTH/auth/users/me \
  -H "Authorization: Bearer $TOKEN"
# → full user profile incl. `is_external_signer` flag

Use /protected/jwt when you just need the claims (faster, no DB join); /auth/users/me when you want role + email + the is_external_signer flag that drives whether on-chain operations require a manual signature from the user's wallet.

Permissions: the auto-fallback gotcha

Every JWT carries a permissions claim listing what the holder can do. For vault-touching operations (mint obligation, send payment, deploy account) the executor checks for the CryptoOperations permission. Without it you get 403 Insufficient permissions: CryptoOperations even though everything else looks right.

The JWT-mint path falls back to role-default permissions when the user_permissions table is empty — so users with the right role get the permission automatically:

Role
SuperAdmin
Gets CryptoOperations from the fallback?
Role
Admin
Gets CryptoOperations from the fallback?
Role
Manager
Gets CryptoOperations from the fallback?
Role
Operator
Gets CryptoOperations from the fallback?
Role
User
Gets CryptoOperations from the fallback?
depends on the deployment's default
Role
Viewer / ApiClient
Gets CryptoOperations from the fallback?
✗ — must be granted explicitly

For users with narrower roles, grant explicitly:

curl -X POST $YF_AUTH/auth/users/$USER_ID/permissions \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "permissions": ["CryptoOperations"] }'

Delegation — acting on behalf of a group

When an operation belongs to a group rather than a single user (group-owned wallet, multi-party deal, working-group settlement), mint a delegation JWT that lets a member of the group act on the group's behalf. The user's identity stays in the audit trail; the acting_as claim records which group the operation belongs to.

curl -X POST $YF_AUTH/auth/delegation/jwt \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "group_id": "550e8400-e29b-41d4-a716-446655440000",
    "delegation_scope": ["read", "write", "manage"],
    "expiry_seconds": 3600
  }'

Send the resulting delegation JWT as the Bearer on operations that should run as the group. Delegation is a JWT claim, not an HTTP header — never try to "act as" via a header.

Manage delegation tokens

# List active delegations
curl $YF_AUTH/auth/delegation/tokens -H "Authorization: Bearer $TOKEN"

# Revoke a delegation
curl -X DELETE $YF_AUTH/auth/delegation/tokens/{token_id} \
  -H "Authorization: Bearer $TOKEN"

JWT structure

User JWT

{
  "sub": "550e8400-e29b-41d4-a716-446655440000",
  "aud": ["yieldfabric"],
  "exp": 1697712000,
  "iat": 1697625600,
  "role": "Operator",
  "permissions": ["CryptoOperations", "ViewSignatureKeys"],
  "session_id": "a1b2c3d4-...",
  "auth_method": "jwt",
  "entity_type": "user",
  "email": "user@example.com",
  "account_address": "0x1234567890abcdef...",
  "default_wallet_id": "...",
  "acting_as": null,
  "delegation_scope": null
}
Claim
sub
Meaning
User ID (UUID).
Claim
role
Meaning
One of SuperAdmin / Admin / Manager / Operator / User / Viewer / ApiClient.
Claim
permissions
Meaning
Effective permissions for this session (auto-filled from role-defaults when empty).
Claim
account_address
Meaning
The user's on-chain account address.
Claim
default_wallet_id
Meaning
The wallet UUID — thread this into mutations that need a wallet reference.
Claim
auth_method
Meaning
jwt (login) / api_key (key exchange) / signature (wallet sign-in) / delegation (delegated).

Delegation JWT (additions)

{
  "auth_method": "delegation",
  "group_account_address": "0xabcdef1234567890...",
  "acting_as": "group-id-550e8400-...",
  "delegation_scope": ["CryptoOperations", "ReadGroup"],
  "delegation_token_id": "c3d4e5f6-..."
}

User roles

Role
SuperAdmin
What it covers
Full system access.
Role
Admin
What it covers
User + group management.
Role
Manager
What it covers
Manage entities and groups, mint delegations.
Role
Operator
What it covers
Use services + manage groups. The most common authoring role.
Role
User
What it covers
Service access for individual operations.
Role
Viewer
What it covers
Read-only.
Role
ApiClient
What it covers
Narrow API integration access.

Common permissions

Permission
CryptoOperations
Description
Required for vault-touching ops (mint, send, deploy). Auto-granted for SuperAdmin / Admin / Manager / Operator via role-fallback.
Permission
ViewSignatureKeys
Description
View signing keys for caller-visible entities.
Permission
ManageSignatureKeys
Description
Generate / rotate / revoke signing keys.
Permission
CreateGroup
Description
Create new groups.
Permission
CreateDelegationToken
Description
Mint delegation tokens for group operations.

Auth endpoints quick reference

Method
POST
Path
/auth/login
Description
Email + password sign-in
Method
POST
Path
/auth/refresh
Description
Rotate access token (single-use refresh)
Method
GET
Path
/auth/signature/nonce
Description
Fetch nonce for wallet sign-in
Method
POST
Path
/auth/signature/signin
Description
Wallet-signature exchange
Method
POST
Path
/auth/{provider}/exchange
Description
Provider exchange (MetaMask, Averer, etc.)
Method
POST
Path
/auth/api-key/generate
Description
Mint a new API key (returns plaintext once)
Method
POST
Path
/auth/api-key
Description
Exchange API key for JWT
Method
GET
Path
/auth/api-keys
Description
List your active API keys
Method
POST
Path
/auth/api-keys/{id}/revoke
Description
Revoke an API key
Method
GET
Path
/protected/jwt
Description
Decode bearer; return claims
Method
GET
Path
/auth/users/me
Description
Full user profile (role, email, is_external_signer)
Method
POST
Path
/auth/delegation/jwt
Description
Mint a delegation token
Method
GET
Path
/auth/delegation/tokens
Description
List active delegations
Method
DELETE
Path
/auth/delegation/tokens/{id}
Description
Revoke a delegation
Method
POST
Path
/auth/users/{id}/permissions
Description
Grant explicit permissions

Full surface (~130 endpoints with OpenAPI types): auth API reference.

Error shape

Auth REST errors are flat: { "error": "<string>" } with the information in the HTTP status. There is no nested code envelope on this side; message strings are human-readable and may change between releases. Route on status code:

Status
401
When
Missing / expired / wrong-audience JWT — refresh, or have the user re-sign-in.
Status
403
When
JWT valid but role/scope/permission insufficient for this operation.
Status
429
When
Rate-limited (login failures only — response delayed but still 401).

GraphQL surfaces use a different shape — see API Reference.

See also

  • Building with YieldFabric — wire-level reference covering all four sign-in paths plus the most-common operations.
  • Balances — using a delegation token to query a group's wallet activity.
  • Auth API reference — endpoint-level details for issuing, exchanging, rotating, and revoking API keys.
YieldFabric docs(317)