Docs/Authentication
Tutorial

Authentication

Every request to the API carries a session token signed with ML-DSA-87 (FIPS 204). This page covers obtaining one and using it.

There are two distinct credentials in the platform, and it is worth separating them up front:

CredentialAnswersLifetime
Session tokenWho are you?24h, whole account
UCAN tokenWhat may you do to this one resource?Minutes to days, one grant

A session token authenticates you. A UCAN grants a narrow, delegable capability over a specific resource. You always need the first; you use the second to share.

1. Register

bash
curl -X POST https://api.ekayana.com/api/signup \
  -H "Content-Type: application/json" \
  -d '{
    "username": "ada",
    "email": "ada@lab.example",
    "password": "correct-horse-battery"
  }'

username must be 3–50 characters, email must parse as an address, and password has a minimum length of 8 plus a complexity rule. An optional name field carries a display name.

json
{ "token": "eyJhbGciOi...<payload>...<signature>" }

2. Sign in

bash
curl -X POST https://api.ekayana.com/api/signin \
  -H "Content-Type: application/json" \
  -d '{ "email": "ada@lab.example", "password": "correct-horse-battery" }'

Returns the same { "token": ... } shape. /api/signup and /api/signin are the only unauthenticated write endpoints.

3. Call an authenticated endpoint

bash
curl https://api.ekayana.com/api/users/me \
  -H "Authorization: Bearer $TOKEN"
json
{ "id": 42, "username": "ada", "email": "", "did": "did:bio:user:42" }

The header must be exactly Authorization: Bearer <token>. A missing header, a malformed one, or a token that fails verification all return an auth error before the handler runs.

Note: /api/users/me returns an empty email - the field is not carried in the token claims. Query the profile directly if you need it.

To find collaborators to share with, GET /api/users/search?q=<query> matches username or email (minimum 2 characters, capped at 10 results) and returns each user's did, which is what the sharing flow consumes.

4. Token structure

A successful signup or signin returns a session token signed with ML-DSA-87 (FIPS 204) - the same post-quantum suite used for capability tokens. The structure is JWS-shaped:

base64url(header) . base64url(claims) . base64url(signature)
json
// header
{ "alg": "ML-DSA-87", "typ": "JWT", "nonce": "…" }

// claims
{ "sub": "42", "iat": 1753228800, "exp": 1753315200, "nonce": "…" }

Present it as a bearer credential on every authenticated request:

bash
curl https://api.ekayana.com/api/users/me \
  -H "Authorization: Bearer <token>"

How verification works

The entire signature is carried in the token and checked against the service public key before any claim is read. Two properties follow:

  • No secret is needed to verify. Read-only replicas and external auditors can validate a token without the ability to mint one.
  • Claims are tamper-evident. Editing sub to impersonate another user invalidates the signature, so the token is rejected before the claim is trusted.
Token lifetime defaults to 24 hours and is configurable via JWT_EXPIRATION_SECS. Expiry is enforced on every request; there is no refresh window, so clients re-authenticate when a token lapses.

See Post-Quantum Security for the signing identity and key management.