Authentication
One identity, every service. auth.nuts.services issues tokens that work across GrubCrawler, Ferricula, Shivvr, and anything else in the fleet that needs auth.
Get a token at auth.nuts.services/dashboard — sign in with email magic-link, Google, or GitHub, then generate an ahp_ API token. The same token works against every service that points GNOSIS_AUTH_URL at https://auth.nuts.services.
Token shapes
Three kinds of tokens, all issued and validated by auth.nuts.services:
| Token | Shape | Lifetime | Use |
|---|---|---|---|
Browser JWT | eyJ... (3-part b64) | 24 hours | Web app sessions after magic-link / OAuth login |
API token | ahp_<opaque> | 1 year | Scripts, AI agents, MCP clients, CI |
Internal HMAC | <b64>.<b64> (2-part) | Seconds | Pre-signed query-string URLs (service-internal) |
API token flow (scripts & agents)
Generate once, use forever (or until you revoke it). Pass as the Bearer header on every request:
curl -X POST https://grub.nuts.services/api/crawl \
-H "Authorization: Bearer ahp_yourtoken" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
The receiving service exchanges your ahp_ token for a fresh JWT at POST auth.nuts.services/auth, extracts your email from the JWT claims, and uses it to partition storage. You don't need to manage refresh — the exchange happens server-side on every authenticated request.
Browser login flow (web apps)
For services with a web UI, redirect users to auth.nuts.services with a return_url back to your callback. nuts-auth handles email magic-link, Google, and GitHub — you handle the token storage afterwards.
- Redirect to:
https://auth.nuts.services/login?return_url=https://your-service.com/auth/callback - User signs in (email magic-link, Google, or GitHub).
- auth.nuts.services redirects back to your
return_urlwith the JWT in the query string:https://your-service.com/auth/callback?token=eyJ... - Stash the token (typically
localStorage.nuts_session_token) and send it as the Bearer header on subsequent API calls.
// On your login button
function login() {
const returnUrl = encodeURIComponent(window.location.origin + '/auth/callback');
window.location.href = `https://auth.nuts.services/login?return_url=${returnUrl}`;
}
// On your /auth/callback page
const token = new URLSearchParams(location.search).get('token');
if (token) {
localStorage.setItem('nuts_session_token', token);
history.replaceState({}, '', '/dashboard');
}
Cross-origin tip: localStorage is per-origin. To open the auth.nuts.services dashboard from another service without forcing a re-login, append your JWT to the URL: https://auth.nuts.services/dashboard?token=<your-jwt>. The auth dashboard auto-captures it.
Validating tokens server-side
Services validate by routing based on token shape. Use the N.U.T.S. integration guide for the validation library, or hand-roll:
| Token starts with | Validate via |
|---|---|
ahp_ | POST auth.nuts.services/auth with token=<ahp_...> form body. Returns a JWT with the user's email in the sub claim. |
eyJ (3 dots) | GET auth.nuts.services/api/verify with Authorization: Bearer <jwt>. Returns the decoded claims if valid; 401 if expired. |
Self-hosted: disabling auth
When running services on a private network or behind a corporate firewall, set DISABLE_AUTH=true to skip token validation entirely. Pass a customer_id in the request body to scope storage:
docker run -p 6792:6792 -e DISABLE_AUTH=true deepbluedynamics/grubcrawler
Don't expose DISABLE_AUTH=true services to the public internet. There's no rate limiting or access control when auth is disabled — anyone who reaches the endpoint can use it.
Per-service auth details: GrubCrawler · N.U.T.S.