Handsal

Authorization, for machines.

A self-hosted OAuth 2.0 server. Your service asks, Handsal signs a token for one audience, and the token expires. An agent waits when a person has to say yes.

A token for tenant acme.
iss
https://auth.example.com/t/acme
aud
https://api.acme.example/orders
scope
orders:read
exp
3600 seconds
sub
app_k7mn2p4qr5s6tvwx2abc
alg
ES256
kid
k_2026-11-07_c4e1

What it is for.

  • billing / orders

    A service calls another.

    Billing needs orders:read and nothing beside it. The token names the orders API. A token minted for the warehouse is refused.

  • agent / refunds

    An agent waits for a person.

    The agent proves itself with a private key, so the secret never sits in a prompt. A read can be granted outright. A refund waits for a person, and the token lasts five minutes.

  • acme / globex

    Each team has its own issuer.

    One server, many tenants. Each has its own keys, applications, and audiences. A token from one tenant does not verify against another.

What a token is allowed to be.

  • One audience

    The resource parameter is required. A token is issued for one API, with only the scopes that application was granted on it.

  • Keys stay with the tenant

    Signing is ES256. The private key is encrypted at rest. Resource servers check the signature against that tenant's JWKS, or ask introspection.

  • You can take it back

    Revoke one token, or every token for an application or a tenant. Introspection honors it at once. A server that only checks the signature waits until expiry, so the lifetime stays short. The default is one hour.

How a call works.

Who is calling
  1. Create a tenant.

    Its issuer is your public URL, then /t/, then the slug.

  2. Name the API and the caller.

    The resource has an audience and scopes. The application receives a client id and a secret. The secret is shown once.

  3. The service requests a token.

    Handsal checks the grant, signs with the tenant's active key, and returns a bearer token.

  1. Register an agent.

    Give it a private key, so no shared secret can leak into a prompt. Tokens default to five minutes.

  2. Set the limits.

    How many scopes one token may carry, how many tokens an hour, and which networks may ask.

  3. The agent asks, and names the task.

    A scope that needs a person waits. Handsal signs after the approval, once. The token says the caller is an agent.

One container. PostgreSQL beside it.

Migrations run when the process starts. The admin UI is served from the same origin. You terminate TLS in a proxy and set the public URL to https.

docker compose -f docker-compose.prod.yml up -d

The server listens on port 3000.