Overview
MangaDB supports OAuth 2.0 login and account linking for three providers: Google, Discord, and GitHub. Users can sign in with a provider (auto-registering on first use) or link a provider to an existing account.
All OAuth routes are prefixed with /auth/oauth.
All OAuth endpoints share a rate limiter: 20 requests per 15 minutes per IP.
Provider Setup
OAuth requires a Client ID and Client Secret for each provider, stored as environment variables in the backend .env file:
OAUTH_GOOGLE_CLIENT_ID=
OAUTH_GOOGLE_CLIENT_SECRET=
OAUTH_DISCORD_CLIENT_ID=
OAUTH_DISCORD_CLIENT_SECRET=
OAUTH_GITHUB_CLIENT_ID=
OAUTH_GITHUB_CLIENT_SECRET=
- Go to Google Cloud Console → Credentials
- Create an OAuth 2.0 Client ID (Web application)
- Configure the OAuth consent screen (External, app name, authorized domain)
- Set Authorized JavaScript Origins to your frontend URL
- Set Authorized Redirect URIs (see table below)
- Copy Client ID and Client Secret to
.env
Discord
- Go to Discord Developer Portal → Applications
- Create a new application
- Navigate to OAuth2, copy Client ID and reset/copy Client Secret
- Add Redirect URLs (see table below)
- Copy values to
.env
GitHub
- Go to GitHub → Settings → Developer Settings → OAuth Apps
- Create a New OAuth App
- Set the homepage URL to your frontend URL
- Set the Authorization callback URL (see table below)
- Copy Client ID and generate/copy Client Secret to
.env
Note: GitHub only allows one callback URL per OAuth app. You may need separate apps for development and production.
Redirect URIs
Each provider needs two callback URLs registered — one for login and one for account linking. The URLs are constructed from the API base URL defined in oauth-config.js.
| Provider | Login Callback | Link Callback |
|---|---|---|
{apiBase}/auth/oauth/google/callback |
{apiBase}/auth/oauth/link/google/callback |
|
| Discord | {apiBase}/auth/oauth/discord/callback |
{apiBase}/auth/oauth/link/discord/callback |
| GitHub | {apiBase}/auth/oauth/github/callback |
{apiBase}/auth/oauth/link/github/callback |
Development (apiBase = http://localhost:4545):
http://localhost:4545/auth/oauth/{provider}/callbackhttp://localhost:4545/auth/oauth/link/{provider}/callback
Production (apiBase = https://api.manga-db.org):
https://api.manga-db.org/auth/oauth/{provider}/callbackhttps://api.manga-db.org/auth/oauth/link/{provider}/callback
Changing the Domain
If the API domain changes (e.g. moving from api.manga-db.org to a different domain):
- Update
backend-config.jsport / domain as needed - The redirect URIs in
oauth-config.jsauto-derive fromDB_ENVand the backend port — no manual URI changes needed - Update the redirect URIs in each provider’s developer console to match the new domain
- Update CORS origins in
server.jsto include the new frontend domain
Login / Register Flow
GET /auth/oauth/:provider
Redirects the user to the provider’s consent screen to begin the OAuth flow.
No authentication required.
| Parameter | Type | Location | Description |
|---|---|---|---|
| provider | string | URL path | google, discord, or github |
- Generates a cryptographic
statetoken (stored server-side, 10 min expiry) - Redirects to the provider’s authorization URL with the configured scopes
- After consent, the provider redirects to the callback URL
302 Redirect
Redirects to the provider’s consent page.
400 Bad Request
Returned if the provider is not supported.
GET /auth/oauth/:provider/callback
Handles the OAuth callback after the user authorizes with the provider.
No authentication required. The state parameter is validated against the server-side store.
| Parameter | Type | Location | Description |
|---|---|---|---|
| provider | string | URL path | google, discord, or github |
| code | string | Query param | Authorization code from provider |
| state | string | Query param | State token for CSRF validation |
- Validates the
statetoken - Exchanges the authorization
codefor an access token - Fetches the user’s profile from the provider
- If the provider account is already linked to a user → logs that user in
- If not → creates a new user account (auto-generates username from provider profile) and links the provider
- Issues JWT tokens and sets the
refreshTokencookie - Redirects to the frontend with
?oauth_success=1
On error, redirects to the frontend with ?oauth_error={reason} (e.g. access_denied, invalid_state, exchange_failed).
302 Redirect
Redirects to the frontend URL:
- Success:
{frontendUrl}/login?oauth_success=1 - Error:
{frontendUrl}/login?oauth_error={reason}
Account Linking
These endpoints allow authenticated users to connect or disconnect OAuth providers to their existing account.
GET /auth/oauth/link/:provider
Starts the account linking flow for an authenticated user.
Requires a valid refreshToken cookie.
| Parameter | Type | Location | Description |
|---|---|---|---|
| provider | string | URL path | google, discord, or github |
- Validates the refresh token
- Generates a
statetoken bound to the user - Redirects to the provider’s consent screen
302 Redirect
Redirects to the provider’s consent page.
401 Unauthorized
Returned if the refresh token is missing or invalid.
GET /auth/oauth/link/:provider/callback
Handles the callback for account linking.
Requires a valid refreshToken cookie. The state parameter is validated.
| Parameter | Type | Location | Description |
|---|---|---|---|
| provider | string | URL path | google, discord, or github |
| code | string | Query param | Authorization code from provider |
| state | string | Query param | State token for CSRF validation |
- Validates
stateand refresh token - Exchanges the code for an access token and fetches the provider profile
- Checks that the provider account isn’t already linked to another user
- Links the provider to the authenticated user’s account
- Redirects to the frontend settings page
302 Redirect
Redirects to the frontend:
- Success:
{frontendUrl}/settings?link_success={provider} - Error:
{frontendUrl}/settings?link_error={reason}(e.g.already_linked,provider_in_use,access_denied)
GET /auth/oauth/connections
Returns the list of OAuth providers linked to the authenticated user’s account.
Requires a valid refreshToken cookie.
200 OK
[
{
"provider": "google",
"provider_username": "John Doe",
"provider_email": "john@gmail.com",
"linked_at": "2026-01-15T10:30:00.000Z"
},
{
"provider": "discord",
"provider_username": "john#1234",
"provider_email": "john@example.com",
"linked_at": "2026-02-20T14:15:00.000Z"
}
]
401 Unauthorized
Returned if the refresh token is missing or invalid.
DELETE /auth/oauth/unlink/:provider
Unlinks an OAuth provider from the authenticated user’s account.
Requires a valid refreshToken cookie.
| Parameter | Type | Location | Description |
|---|---|---|---|
| provider | string | URL path | google, discord, or github |
Validates that the user has an alternative login method before allowing the unlink:
- If the user has a password set → unlink is allowed
- If the user has no password and this is their only linked provider → unlink is rejected (would lock them out)
200 OK
Provider successfully unlinked.
400 Bad Request
Returned if unlinking would leave the user with no way to log in.
401 Unauthorized
Returned if the refresh token is missing or invalid.
404 Not Found
Returned if the provider is not linked to this account.
Configuration Reference
All OAuth settings live in src/config/oauth-config.js. The redirect URIs are auto-generated based on environment:
| Environment Variable | Local Value | Production Value |
|---|---|---|
DB_ENV |
local |
anything else |
| API Base URL | http://localhost:{port} |
https://api.manga-db.org |
| Frontend URL | http://localhost:3000 |
https://manga-db.org |
Provider Scopes
| Provider | Scopes |
|---|---|
openid, profile, email |
|
| Discord | identify, email |
| GitHub | read:user, user:email |
Database Table
OAuth connections are stored in the oauth_account table:
| Column | Type | Description |
|---|---|---|
| id | int | Auto-increment primary key |
| user_id | varchar | Foreign key to user.id |
| provider | varchar | Provider name (google, discord, github) |
| provider_user_id | varchar | User’s ID on the provider |
| provider_username | varchar | Display name from the provider |
| provider_email | varchar | Email from the provider (nullable) |
| provider_avatar | varchar | Avatar URL from the provider (nullable) |
| linked_at | datetime | Timestamp when the account was linked |