Authentication
Lipafy accepts five credential types. They all converge on the same req.auth shape — routes don’t branch on credential type, only on whether requireAuth(req) or requireAdmin(req) succeeds.
The five credential types
The first three are HTTP Bearer; the fourth is a cookie automatically forwarded by the browser; the fifth is request-body data validated server-side.
API keys
Returned exactly once in plaintext. Stored only as SHA-256(salt || plaintext). Pattern: lip_live_<64 hex>. Each API key gets a paired grants row so it has its own spending controls.
Revoke via DELETE /v1/accounts/me/api-keys/:id. Subsequent requests with that key return api_key_revoked.
OAuth 2.1 (for agents)
Lipafy is a standards-compliant OAuth 2.1 authorization server. The full surface:
PKCE S256 is required for all flows; plaintext code_verifier is rejected. Refresh tokens rotate on every use; reuse of an old refresh token revokes the whole grant (standard detect-and-revoke).
The auth plugin verifies every access token against oauth_grants.revoked_at on every request, so revocation is instant — not “next refresh.”
Sign-In with Ethereum (SIWE) — wallet login
The recovered EVM address is uniquely tied to a Lipafy account in wallet_addresses. The same wallet can sign approvals and intent mandates later — see the relevant concept pages.
Same email across providers — what happens
If you signed up with Google using you@example.com, then later sign in with GitHub whose primary verified email is also you@example.com, Lipafy automatically links them into one account. You keep the same balance, capabilities, and history.
The matching rule, in order:
- Exact identity match wins. If
(provider, sub)is already linked, that account is returned, period. - Verified email match. If no identity is linked yet but the provider returns
email_verified=trueand an account already exists with that email, Lipafy links the new identity to it. Both Google and GitHub mark their primary emails as verified; this path catches the common “same person, different provider” case. - Otherwise create a new account. If neither match applies, a fresh account is created.
If you ever end up with two separate accounts and want to merge balance + history, that’s a manual support ticket today — Lipafy doesn’t expose self-serve account merging because doing it correctly with active wallets is hard.
Linking multiple identities to one account
Lipafy supports attaching more than one sign-in method to the same account. A user who first signed up with Google can later add a wallet (and vice versa) — they stay on one Lipafy account with one balance, one history, and one identity for approvals.
Auto-link: the regular sign-in endpoints are session-aware. If you call GET /v1/auth/google/start or POST /v1/auth/wallet/verify while already signed in (session cookie or Bearer token present), Lipafy treats the request as a link rather than a fresh sign-in. The result is:
- New identity → attached to your current account.
- Already attached to your current account → idempotent success.
- Already attached to a different account →
409 conflict.
So the user-facing pattern is just: “sign in with the other method while signed in.” The dashboard exposes this on /account as Link Google / Link wallet buttons that hit the same routes.
Explicit endpoints (programmatic clients):
The last-identity guard prevents users from locking themselves out: you can’t remove your final sign-in method. Link another first.
Sessions (browsers)
After Google or wallet sign-in, Lipafy sets a lipafy_session cookie. The cookie holds an opaque token; only the SHA-256 hash is stored in sessions. Logging out flips sessions.revoked_at and the cookie stops working on the next request, not at TTL.
Cookie attributes: HttpOnly; Secure (in prod); SameSite=Lax; Path=/. Configurable domain via SESSION_COOKIE_DOMAIN.
Wallet signatures (on specific protected actions)
For specific high-stakes actions Lipafy supports purely signature-based authorization, where the request body carries a signature over a server-canonical message:
- Approvals:
POST /v1/approvals/:id/decide-by-signature - Intent mandates:
POST /v1/intent-mandates(create flow)
These don’t require a Bearer header or session cookie. The signature itself authorises the action, validated against the wallet linkage on the owning account.
