The backend of Open Authenticator.
Installation »
Website
·
App
·
Backend
·
Contribute
Open Authenticator is a free, open-source and cross-platform TOTP manager. This repository contains the source code of its backend, allowing you to have access to your TOTPs on all your devices.
Tip
If you like this project, consider starring it on GitHub !
Open Authenticator Backend is powered by Nitro. You will need the following in order to be able to run it :
- A Node.js environment. Either a server or a serverless environment (eg. Cloudflare, Vercel, ...).
- A database. The connector should be compatible with DB0 (see all available connectors here). This is where all users' information will be stored.
- A storage. The connector should be compatible with unstorage (see all available drivers here). This is where all TOTPs will be stored.
- Optionally, an email account, for sending magic links.
- Optionally, a key-value storage provider, for storing rate limiting related data.
The backend is still in development. To install it, currently, you only have to clone the repository and build it.
git clone https://github.com/openauthenticator-app/backend.git
cd backend
npm install
npm run buildAnd to start it :
npm run startTo configure the backend, you'll have to edit backend.config.ts. For example, to host it on Cloudflare, you may want to configure it like this :
// noinspection ES6PreferShortImport
import { defineBackendConfig } from './utils/config'
export default defineBackendConfig({
enableRegistrations: false, // You can disable new user registrations if needed.
totps: {
storage: {
driver: 'cloudflare-r2-binding',
binding: 'BUCKET',
},
},
authentication: {
database: {
connector: 'cloudflare-d1',
options: {
// @ts-expect-error `bindingName` is not in the type definition.
bindingName: 'DATABASE',
},
},
providers: {
email: {
library: 'workermailer',
host: 'smtp.example.com',
port: 587,
username: 'noreply@example.com',
password: process.env.EMAIL_PASSWORD
},
},
},
rateLimiter: {
storage: {
driver: 'cloudflare-kv-binding',
binding: 'STORAGE',
},
},
})with bindings configured in a wrangler.json file. You may also need to configure some environment variables :
NODE_ENV='production' # You should be in production.
URL='https://example.com' # Your backend URL.
ADMIN_HEADER='Bearer SECURE_RANDOM_STRING' # Required to access /admin/* routes.
JWT_ACCESS_SECRET='ANOTHER_SECURE_RANDOM_STRING' # Used to encrypt access tokens.
JWT_REFRESH_SECRET='ANOTHER_ANOTHER_SECURE_RANDOM_STRING' # Used to encrypt refresh tokens.
JWT_REFRESH_PEPPER='ANOTHER_ANOTHER_ANOTHER_SECURE_RANDOM_STRING' # Used to encrypt refresh tokens.
EMAIL_PASSWORD='YOUR_PASSWORD' # Used in the example above to authenticate your email address.The following table lists all available configuration options, along with their default values.
Use backend.config.ts to override them.
| Option | Default | Description |
|---|---|---|
url |
process.env.URL |
Public backend URL, used to build OAuth callbacks and magic links. |
appVersionRange |
>=2.0.0 <3.0.0 |
Supported application version range checked against the App-Version header. |
enableRegistrations |
true |
Allows creating new users when a valid login does not match an existing account. |
adminHeader |
process.env.ADMIN_HEADER |
Expected Authorization header for /admin/* routes. |
totps.storage |
{ driver: 'memory' } |
Unstorage-compatible storage configuration for encrypted TOTP data. |
totps.limit.default |
6 |
Maximum number of TOTPs for regular users. |
totps.limit.contributor |
100 |
Maximum number of TOTPs for contributor users. |
authentication.strategy |
hybrid |
Session strategy. stateless uses JWTs only, hybrid stores refresh sessions but validates access tokens without a DB lookup, and stateful checks the session database for access tokens too. |
authentication.database |
SQLite db |
DB0-compatible database used for users, stateful/hybrid refresh sessions, and email verification records. |
authentication.tokensTtl.access |
15m |
Access token lifetime. |
authentication.tokensTtl.refresh |
60d |
Refresh token lifetime. |
authentication.cookiesOptions |
HttpOnly, Secure, SameSite=Lax, path / |
Cookie options used during OAuth redirect flows. |
authentication.jwtSecrets.access |
process.env.JWT_ACCESS_SECRET |
Secret used to sign access tokens. |
authentication.jwtSecrets.refresh |
process.env.JWT_REFRESH_SECRET |
Secret used to sign refresh tokens. |
authentication.jwtSecrets.refreshPepper |
process.env.JWT_REFRESH_PEPPER |
Pepper used to hash refresh tokens before storing them when stateful auth is enabled. |
authentication.providers.google |
Environment variables | Google OAuth client configuration. |
authentication.providers.apple |
Environment variables | Apple OAuth client configuration. |
authentication.providers.github |
Environment variables | GitHub OAuth client configuration. |
authentication.providers.microsoft |
Environment variables | Microsoft OAuth client configuration. |
authentication.providers.email |
nodemailer with environment defaults |
Email provider used for magic-link/code login. Supports the mailer libraries exposed by the backend email module. |
sentryDsn |
process.env.SENTRY_DSN |
Optional Sentry DSN. |
revenueCat.contributorPlanEntitlementId |
contributor_plan |
RevenueCat entitlement ID used to identify contributor users. |
revenueCat.authorizationHeader |
process.env.REVENUECAT_AUTHORIZATION_HEADER |
Expected Authorization header for RevenueCat webhooks. |
rateLimiter.enable |
true |
Enables route rate limiting. |
rateLimiter.storage |
{ driver: 'memory' } |
Unstorage-compatible storage configuration for rate-limit counters. |
Please refer to the default config.
Note
Don't forget to rebuild the server after each configuration change.
To (re)create the default tables, send a POST request to /admin/reset with your previously defined ADMIN_HEADER set as the Authorization header. To prune unnecessary data, send a POST request to /admin/prune. You can also prune only one category with /admin/prune/accounts, /admin/prune/sessions, /admin/prune/totps, or /admin/prune/revenuecat.
Most application routes require two headers :
App-Version, which must satisfyappVersionRange.App-Client-Id, a stable identifier for the current app installation/client.
These headers are required for /auth/*, /totps/*, /user/*, and /ping, except OAuth and email callback/redirect endpoints. Admin routes additionally require the configured adminHeader as the Authorization header.
| Route | Method | Purpose | Authentication |
|---|---|---|---|
/ping |
GET |
Health/version-compatible ping endpoint. | App headers |
/auth/provider/:provider/redirect |
GET |
Starts an OAuth or email login/link flow. Supported providers are google, apple, microsoft, github, and email depending on what configured in backend.config.ts. |
Public, provider-specific |
/auth/provider/:provider/callback |
GET |
Handles OAuth provider redirects and returns an app deep link. | Public, protected by OAuth state/PKCE cookies where applicable |
/auth/provider/:provider/callback |
POST |
Handles Apple and email callbacks that post data. Sends a redirection for Apple. | Provider-specific |
/auth/provider/:provider/login |
POST |
Exchanges a provider authorization code for backend accessToken and refreshToken. |
App headers |
/auth/provider/:provider/link |
POST |
Links a provider identity to the current user. | Bearer access token |
/auth/provider/:provider/unlink |
POST |
Unlinks a provider identity from the current user. | Bearer access token |
/auth/provider/:provider/cancel |
POST |
Cancels a pending email login request. | Email provider only |
/auth/refresh |
POST |
Exchanges a refresh token for a new token pair. | Refresh token + app headers |
/auth/logout |
POST |
Revokes the refresh token in stateful mode. In stateless mode, validates the token but cannot revoke it server-side. | Refresh token + app headers |
/user |
GET |
Returns the current user profile. | Bearer access token |
/user |
DELETE |
Deletes the current user, sessions, and TOTP data. | Bearer access token |
/totps |
GET |
Returns all encrypted TOTPs for the current user. | Bearer access token |
/totps |
POST |
Replaces/sets encrypted TOTPs in bulk. | Bearer access token |
/totps |
DELETE |
Clears all encrypted TOTPs for the current user. | Bearer access token |
/totps/:uuid |
POST |
Sets one encrypted TOTP. | Bearer access token |
/totps/:uuid |
DELETE |
Deletes one encrypted TOTP and stores a tombstone for sync. | Bearer access token |
/totps/sync/pull |
POST |
Returns inserts, updates, and deletes newer than the client-known timestamps. | Bearer access token |
/totps/sync/push |
POST |
Applies compacted client sync operations with timestamp conflict checks. | Bearer access token |
/admin/reset |
POST |
Drops and recreates database tables and indexes. | adminHeader |
/admin/prune |
POST |
Prunes inactive accounts, expired sessions, deleted TOTP tombstones, and processed RevenueCat webhook events. | adminHeader |
/admin/prune/accounts |
POST |
Prunes inactive non-contributor accounts. Accepts an optional days value in the JSON body. |
adminHeader |
/admin/prune/sessions |
POST |
Prunes expired sessions. | adminHeader |
/admin/prune/totps |
POST |
Prunes deleted TOTP tombstones. Accepts an optional days value in the JSON body. |
adminHeader |
/admin/prune/revenuecat |
POST |
Prunes processed RevenueCat webhook events. Accepts an optional days value in the JSON body. |
adminHeader |
/webhooks/revenuecat |
POST |
Handles RevenueCat subscription events. | RevenueCat Authorization header |
Authentication is provider-based. OAuth providers use state cookies, and providers that support PKCE store a temporary code verifier cookie during the redirect flow. The provider callback returns an app deep link containing a provider authorization code. The app then calls /auth/provider/:provider/login, and the backend validates that authorization code before issuing backend tokens.
The email provider sends a verification code and magic link. A valid email callback creates a short-lived backend authorization code, which is then exchanged through the same /auth/provider/email/login endpoint.
Provider linking uses the same provider validation path as login, but requires an existing bearer access token. A user cannot unlink the last remaining provider from their account.
The backend issues two JWTs :
- An access token, sent as
Authorization: Bearer <token>for protected routes. - A refresh token, sent in the JSON body to
/auth/refreshand/auth/logout.
Both tokens include the user id as sub, a session id as sid, the token kind as typ, and the app client id as aci. The typ claim must match the expected token kind (access or refresh), and the aci claim must match the App-Client-Id header.
With authentication.strategy = 'stateless', the backend does not write login or refresh sessions to the sessions table. Access and refresh tokens are accepted based on JWT signature, expiration, token kind, and app-client binding. This mode avoids session database I/O, but logout cannot revoke already issued tokens server-side; invalidation relies on token expiration or JWT secret rotation.
With authentication.strategy = 'hybrid', access tokens are validated from the JWT only, while refresh tokens remain backed by the sessions table. The backend stores only a peppered hash of the refresh token, together with the user id, app client id, and expiration timestamp. Refreshing deletes the previous session row and creates a new one. Logout deletes the matching session row. Already issued access tokens remain valid until they expire.
With authentication.strategy = 'stateful', refresh tokens use the same stateful storage as hybrid, and access tokens are also checked against the sessions table on protected requests. Logout and refresh invalidation therefore take effect immediately for access tokens too, at the cost of one database lookup per authenticated request.
Rate limiting is controlled by rateLimiter.enable and uses the configured rateLimiter.storage through Unstorage. Keys are scoped by client IP and route path by default. Responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset; limited responses include Retry-After and return 429.
The default middleware uses a sliding-window algorithm. Most route-specific limits use the default one-minute window. Notable custom limits include :
/auth/provider/:provider/redirect: 3 requests per 15 minutes for email, keyed by IP, path, and normalized email; 20 requests per 15 minutes for other providers./auth/provider/:provider/login: 3 requests for email, 10 for other providers./auth/refresh: 5 requests./totpsbulk operations: 5 requests./userdelete: 1 request.
/webhooks/revenuecat verifies the incoming Authorization header against revenueCat.authorizationHeader using a timing-safe comparison. If subscriber attributes include a backend value, it must match the configured backend hostname.
Handled RevenueCat events update the local contributorPlan flag :
INITIAL_PURCHASE,RENEWAL, andTEMPORARY_ENTITLEMENT_GRANTgrant contributor access when the event containsrevenueCat.contributorPlanEntitlementId.EXPIRATIONremoves contributor access for that entitlement.TRANSFERremoves contributor access from every previous RevenueCat user and grants it to every new one when those users exist locally.- Unknown event types are accepted but only logged.
Processed RevenueCat events are tracked in the revenueCatWebhookEvents table, one row per event and per affected user. This makes retries idempotent and prevents an older webhook from overriding a newer subscription state.
To use your own backend in the app, you'll have to go to the settings, and then choose Change backend URL. Put your own backend URL here, et voilà !
Contributions are more than welcome. For setup details, contribution rules and PR expectations, read the guidelines.
You can also help by :
- reporting bugs or suggesting features in the issue tracker ;
- submitting fixes for documentation, UI text or code.
If you want to support Open Authenticator financially, you can use :
Open Authenticator Backend is licensed under the GNU General Public License v3.0.