Skip to main content

Authentication (JWKS)

storage-service does not issue tokens. It verifies Bearer JWTs created by identity-service using the identity public JWKS document (RSA keys). That document is not a private certificate; it is safe to copy into Coolify or a local file.

Production: store the keys locally​

Do not fetch https://id.shellui.com/.well-known/jwks.json from a container on the same host. That public URL hairpins and times out.

From a laptop (outside the server):

curl -sS https://id.shellui.com/.well-known/jwks.json

Then set one of these on storage-service and restart:

# Coolify env (easiest)
IDENTITY_JWKS={"keys":[...paste the JSON...]}

# Or a file on the data volume
IDENTITY_JWKS_FILE=/app/data/jwks.json

When either is set, storage never calls identity at runtime. After identity rotates signing keys, update the JSON and restart storage.

GET /storage/v1/health is public and returns status and version only. With a valid Bearer JWT, the same endpoint also reports identity_jwks_source (env, file, or url), identity_jwks_url, and storage_backend.

Local/dev: fetch from identity​

Set the identity base URL; JWKS is derived automatically:

IDENTITY_SERVICE_URL=http://localhost:8000
# → fetches http://localhost:8000/.well-known/jwks.json

Override only when JWKS is on a different host:

IDENTITY_JWKS_URL=http://host.docker.internal:8000/.well-known/jwks.json

If IDENTITY_JWKS_FILE or IDENTITY_JWKS is set, it wins and the URL is ignored.

Configuration reference​

VariablePurpose
IDENTITY_JWKS_FILEPath to a JWKS JSON file (production, preferred with a volume)
IDENTITY_JWKSJWKS JSON inline (production, easy in Coolify)
IDENTITY_SERVICE_URLIdentity base URL; derives {url}/.well-known/jwks.json when no local document / explicit JWKS URL
IDENTITY_JWKS_URLOptional fetch URL override (different host than IDENTITY_SERVICE_URL)
IDENTITY_ISSUERRequired when DEBUG=false. Must match identity-service JWT_ISSUER (0.5.0+), e.g. https://id.shellui.com
IDENTITY_AUDIENCERequired when DEBUG=false. Must match identity-service JWT_AUDIENCE (0.5.0+), typically shellui
JWKS_CACHE_TTLSeconds to cache a fetched JWKS (default 900; unused for local documents)
JWKS_TIMEOUTSeconds to wait for a JWKS HTTP response (default 15; connect cap 5)
JWKS_RETRIESExtra JWKS fetch attempts after timeout / 5xx (default 2; 4xx is not retried)
JWT_HS256_FALLBACK_SECRETDev-only: verify HS256 when JWKS has no keys (identity DEBUG=true). Rejected at startup if DEBUG=false unless ALLOW_JWT_HS256_FALLBACK=true
ALLOW_JWT_HS256_FALLBACKExplicit escape hatch to allow HS256 fallback when DEBUG=false (not recommended in production)
JWT_ALGORITHMSDefault RS256

Expected claims​

ClaimUse
sub / user_idOwner id for uploads and per-user quotas
company_idTenancy — buckets and objects are scoped to this company
emailInformational
user_metadata.is_staffQuota admin APIs
user_metadata.is_company_ownerQuota admin for own company

Client headers​

Authorization: Bearer <access_token>

Optional Supabase-style apikey header is accepted by CORS but ignored for authorization.

Debugging failed tokens​

API clients still get a generic 401 (Token is invalid or could not be verified against identity JWKS.) so the token is not leaked. The process logs have the details.

Look for JWT on the apps.authapi.authentication / apps.authapi.jwks_client loggers. Useful fields:

Log fieldWhat it tells you
alg / kidToken header. HS256 with no fallback means identity is in DEBUG mode — set JWT_HS256_FALLBACK_SECRET. kid missing from jwks_kids means storage's JWKS is stale; recopy IDENTITY_JWKS.
iss / aud / expUnverified claims (signature not trusted). In production, iss/aud must match IDENTITY_ISSUER / IDENTITY_AUDIENCE.
jwks_source / jwks_kidsWhere keys were loaded (env, file, or url) and which kids storage currently has.
request_id / [req=…]Correlate one HTTP call. The 401 JSON includes request_id; the same value is in X-Request-ID.

How to read logs:

# Local runserver — logs print in that terminal
uv run python manage.py runserver 8001

# Docker
docker logs -f <container> 2>&1 | grep JWT

# Coolify / systemd — open the service logs and search for "JWT authentication failed"

On startup, storage logs Identity JWKS auth ready: source=… key_count=… kids=…. If key_count=0 and you are not using HS256 fallback, every request will 401.

When DEBUG=true, the 401 detail string also appends the PyJWT exception (InvalidSignatureError, DecodeError, missing kid, …).

WebDAV​

Third-party WebDAV clients may send:

  • Authorization: Bearer <jwt>, or
  • HTTP Basic where the password is the JWT (username can be the user email).