-
Notifications
You must be signed in to change notification settings - Fork 0
Authentication
MangaCollector supports two sign-in flows:
- Google OAuth 2.0 — quickest setup if you have a Google Cloud account.
- Generic OpenID Connect — works with any compliant IdP (Defguard, Auth0, Keycloak, Authentik, Authelia, Okta, …).
Both write to the same users table and produce identical sessions. The
choice is purely operational.

- User clicks the sign-in button on
/log-in. - Browser redirects to the provider's authorisation endpoint with the
redirect_uri set to
${FRONTEND_URL}/auth/callback. - User authenticates on the provider's domain.
- Provider redirects back with an
?code=…&state=…query string. - Backend exchanges the code for an ID token, validates it, looks up or creates the user in Postgres.
- Backend issues a session cookie (
Lax,HttpOnly,Securein production) tied to a row insessions. - Browser is redirected to
/dashboard.
The session lives in Postgres via tower-sessions-sqlx-store. There's no
JWT — sessions are server-side and revocable from the Active sessions
modal in the profile dropdown.
The fastest path. Five-minute setup.
- Open the Google Cloud Console — Credentials
- Create credentials → OAuth client ID
- Application type: Web application
- Authorised redirect URIs: add
${FRONTEND_URL}/auth/callbackfor every environment you plan to deploy. Examples:-
http://localhost:12000/auth/callback(dev) -
https://mangacollector.example.com/auth/callback(prod)
-
- Create. Copy the client ID and the client secret.
AUTH_MODE=google
AUTH_CLIENT_ID=<from-google-console>
AUTH_CLIENT_SECRET=<from-google-console>
AUTH_NAME=Google
AUTH_ICON=google
SESSION_SECRET=<openssl rand -base64 48>
FRONTEND_URL=https://mangacollector.example.comThat's it. Restart the backend and you can sign in.
Out of the box the backend accepts any successful Google sign-in — anyone with a Gmail account can create a profile. Two ways to lock that down:
- Hosted-domain restriction — for Google Workspace tenants, set up the consent screen as Internal instead of External. Google then refuses sign-ins from outside your domain at the provider level.
- External access list — a database-level allowlist isn't shipped today; for personal deployments, restricting OAuth credentials to your own Google account during creation is sufficient.
Works with any provider that exposes /.well-known/openid-configuration
with an OIDC-compliant document.
The exact UI varies by provider, but you'll always end up with:
- a Client ID + Client Secret
- a list of allowed redirect URIs — add
${FRONTEND_URL}/auth/callback - the issuer URL (your IdP's base URL where it serves
/.well-known/openid-configuration)
The required scopes are openid email profile. The backend doesn't request
custom scopes.
AUTH_MODE=openidconnect
AUTH_ISSUER=https://idp.example.com
AUTH_CLIENT_ID=<from-idp>
AUTH_CLIENT_SECRET=<from-idp>
AUTH_NAME="My SSO"
AUTH_ICON=https://idp.example.com/logo.svg
SESSION_SECRET=<openssl rand -base64 48>
FRONTEND_URL=https://mangacollector.example.comThese have been verified end-to-end:
- Defguard (the dev compose example uses this)
- Authentik
- Keycloak (≥ 21)
- Auth0
If your IdP exposes the discovery document at a non-standard path, set
AUTH_ISSUER_BASE_PATH to the prefix that gets appended after the issuer
host. Example for Defguard:
AUTH_ISSUER=https://idp.example.com
AUTH_ISSUER_BASE_PATH=/api/v1/oauth/The backend will fetch
https://idp.example.com/api/v1/oauth/.well-known/openid-configuration
instead of the default location.
AUTH_NAME and AUTH_ICON control the button shown on /log-in:
AUTH_NAME="Acme SSO"
AUTH_ICON=https://acme.example.com/sso.svgAUTH_ICON accepts the literal value google (built-in glyph) or any URL
pointing to a PNG / SVG.
Sessions are persisted in Postgres (sessions table). Cookies are:
-
HttpOnly(no JS access) -
SameSite=Lax(mitigates CSRF for cross-site GETs) -
Secure(only whenFRONTEND_URLstarts withhttps://) - 14-day rolling lifetime — every authenticated request bumps the expiry
The profile-button dropdown exposes Active sessions — opens a modal listing every device currently signed in (UA-based icon, IP address, last seen). The user can revoke any session from there; the affected device is forced to re-sign-in on its next request.
A user can be force-signed-out everywhere by deleting their rows from the
sessions table:
DELETE FROM sessions WHERE user_id = (SELECT id FROM users WHERE email = '...');The next request from any of their tabs receives a 401, and the frontend
redirects to /log-in.
| Symptom | Likely cause |
|---|---|
OAuth callback returns 400 redirect_uri_mismatch
|
The redirect URI in your IdP doesn't match ${FRONTEND_URL}/auth/callback exactly — including scheme and trailing slash. |
Sign-in succeeds but you land back on /log-in
|
Check Set-Cookie is actually being set. In production this requires https:// for Secure cookies; with HTTP, set FRONTEND_URL=http://... and the cookie will drop the Secure flag. |
Failed to fetch OIDC discovery document in logs |
AUTH_ISSUER (+ optional AUTH_ISSUER_BASE_PATH) doesn't resolve to a /.well-known/openid-configuration. Try opening it in your browser. |
invalid_client from the IdP |
Client ID or secret is wrong, or the client is configured for a different redirect_uri. |
🛠️ Infrastructure
- Architecture
- Quick start (Docker)
- Production
- Local development
- Environment variables
- Authentication
- Storage
- Database & migrations
👤 User guide
- Library
- Adding a series
- Series page
- Volume editing
- Coffrets
- Calendar
- Seals
- Profile & stats
- Public profile
- Settings
- Import / export
🔌 Release proxy
📚 Reference