WebApp Modules

OAuth2 Server

Turn your WebApp into a standards-compliant OAuth2 authorization server. Third-party applications can sign in your users and access their data with the Authorization Code flow, PKCE, scopes, and refresh tokens.

Overview

The OAuth2 Server module (@dashnex/oauth2-server) lets your WebApp act as an identity provider. External applications redirect users to your WebApp to sign in, the user grants consent, and the app receives tokens it can use to call your API on the user's behalf — the same pattern as "Sign in with Google" or "Sign in with GitHub".

The module implements the OAuth2 Authorization Code grant (with optional PKCE) and the Refresh Token grant. It ships with:

  • A set of standard OAuth2 endpoints (authorize, token, userinfo)
  • A hosted consent screen where users approve or deny each application
  • An admin UI for registering and managing OAuth clients
  • A per-user list of active sessions that users can revoke at any time
  • An hourly background job that cleans up expired codes and tokens

Key concepts

TermMeaning
ClientA registered third-party application, identified by a client_id and a secret client_secret.
ScopeA named permission the client requests (e.g. profile). Clients can only request scopes they were registered with.
Authorization codeA short-lived, one-time code (10 min) returned after consent, then exchanged for tokens.
Access tokenA signed JWT (HS256) used as a Bearer token. Default lifetime 1 hour.
Refresh tokenAn opaque token used to obtain a new access token. Rotated on every use. Default lifetime 30 days.
PKCEProof Key for Code Exchange — protects public clients. Supports S256 and plain.

Configuration

The module reads the following environment variables. OAUTH2_JWT_SECRET is required — it signs access tokens and encrypts stored client secrets.

VariableDefaultDescription
OAUTH2_JWT_SECRETSecret used to sign access-token JWTs and encrypt client secrets at rest. Required.
OAUTH2_ACCESS_TOKEN_EXPIRY3600Default access-token lifetime in seconds (1 hour). Overridable per client.
OAUTH2_REFRESH_TOKEN_EXPIRY2592000Default refresh-token lifetime in seconds (30 days). Overridable per client.
NEXT_PUBLIC_DASHNEX_APP_DOMAINYour WebApp domain, used by the client-side consent UI.

The module also registers two roles for its management API — oauth2:clients:read and oauth2:clients:write (write implies read) — and schedules an hourly cron (0 * * * *) that purges expired authorization codes and tokens.

Registering an OAuth client

In your WebApp admin, open Settings → Authentication. The OAuth Clients panel lists your registered applications. Click New to register one and fill in:

  • Name — shown to users on the consent screen.
  • Redirect URIs — one or more exact callback URLs. Codes are only returned to a URI on this list.
  • Scopes — permissions this client may request. profile is predefined; you can add your own.
  • Grantsauthorization_code and/or refresh_token.
  • Access / refresh token TTL — per-client overrides for the env defaults.
  • Require PKCE — reject any authorization request without a code challenge.

Save the client secret immediately. The plaintext secret is shown only once, at creation time. It is stored encrypted (AES-GCM) and can be retrieved again from the admin UI, but if it is ever lost you can delete and recreate the client.

The authorization flow

A complete Authorization Code flow looks like this:

  1. Your app redirects the user to /api/oauth/authorize.
  2. The server validates the client and redirect URI, then forwards the user to the consent screen.
  3. The user signs in (if needed) and approves or denies access.
  4. On approval, the user is redirected to your redirect_uri with a code (and your state).
  5. Your app exchanges the code at /api/oauth/token for an access token (and optional refresh token).
  6. Your app calls /api/oauth/userinfo or your API with the access token.

1. Send the user to authorize

GET https://your-webapp.com/api/oauth/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://your-app.com/callback
  &scope=profile
  &state=RANDOM_STATE
  &code_challenge=BASE64URL_SHA256_OF_VERIFIER
  &code_challenge_method=S256

response_type must be code. scope, state, and the PKCE parameters are optional (PKCE is required if the client has Require PKCE enabled). If scope is omitted, the client's default scopes are used.

2. The user grants consent

The server redirects to the hosted consent page (/oauth/consent), which shows the application name and the list of requested scopes. Unauthenticated users are sent to log in first, then returned to consent. On approval the user is redirected back to your callback:

https://your-app.com/callback?code=AUTH_CODE&state=RANDOM_STATE

On denial, the user is redirected back with error=access_denied instead. Always verify that the returned state matches the value you sent.

3. Exchange the code for tokens

POST https://your-webapp.com/api/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&redirect_uri=https://your-app.com/callback
&code=AUTH_CODE
&code_verifier=ORIGINAL_PKCE_VERIFIER

The token endpoint accepts both application/x-www-form-urlencoded and JSON bodies. A successful response:

{
  "access_token": "eyJhbGciOiJIUzI1NiI...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "a1b2c3...",
  "scope": "profile"
}

refresh_token is only included when the client is allowed the refresh_token grant. Authorization codes are single-use and expire after 10 minutes.

4. Call the API with the access token

GET https://your-webapp.com/api/oauth/userinfo
Authorization: Bearer eyJhbGciOiJIUzI1NiI...

Returns the authenticated user's profile:

{
  "sub": "user_123",
  "id": "user_123",
  "email": "jane@example.com",
  "firstName": "Jane",
  "lastName": "Doe"
}

Refreshing tokens

When an access token expires, exchange the refresh token for a new pair. Refresh tokens are rotated — the old refresh token is revoked and a new one is returned, so always store the latest value.

POST https://your-webapp.com/api/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&refresh_token=CURRENT_REFRESH_TOKEN

Endpoint reference

Method & pathAuthPurpose
GET /api/oauth/authorizePublicStart the flow; redirect to consent.
POST /api/oauth/tokenClient credentialsExchange a code or refresh token for tokens.
GET /api/oauth/userinfoBearer tokenReturn the token owner's profile.
GET /api/oauth/sessions/meUserList the current user's active sessions.
DELETE /api/oauth/sessions/me/:idUserRevoke one of the current user's sessions.
GET /api/oauth/sessionsAdminList all active sessions.
DELETE /api/oauth/sessions/:idAdminRevoke any session.
GET · POST · PUT · DELETE /api/oauth/clientsAdminManage OAuth clients (backs the admin UI).

Session management & revocation

Every issued access token is recorded as a session. Users can review and revoke their own sessions from the Authorization tab of their account settings — the module adds an Active Sessions widget there. Administrators can list and revoke any session across the WebApp.

Revoking a session immediately invalidates the access token and its associated refresh token. Expired codes and tokens are also cleaned up automatically by the hourly background job.

Client management API

In addition to the admin UI, OAuth clients can be managed programmatically through the WebApp's external API. These routes are documented in your WebApp's OpenAPI spec under the OAuth2 Clients tag and are protected by the oauth2:clients:read and oauth2:clients:write roles.

  • GET /oauth/clients — list clients (secrets hidden).
  • POST /oauth/clients — create a client; the plaintext secret is returned only in this response.
  • GET /oauth/clients/:id — fetch a single client.
  • PUT /oauth/clients/:id/redirect-uris — replace the redirect URI list.
  • DELETE /oauth/clients/:id — delete a client.

Security notes

  • Exact redirect-URI matching. Codes are only issued to URIs in the client's registered list — no wildcards or prefixes.
  • Scope enforcement. A client can never request a scope it was not registered with.
  • PKCE. Enable Require PKCE for public clients (mobile apps, SPAs) that cannot keep a secret confidential.
  • Encrypted secrets. Client secrets are encrypted with AES-GCM derived from OAUTH2_JWT_SECRET, never stored in plaintext.
  • One-time codes. Authorization codes are deleted on first use and expire after 10 minutes.
  • Refresh rotation. Refresh tokens are revoked and reissued on each refresh, limiting the window of a leaked token.

Extending the flow (for module developers)

The OAuth2 Server stays scope-agnostic and emits events so other modules can hook into the flow — for example, to route a specific scope to a custom consent page or to issue a different kind of token. The following events are available:

EventWhen & what a listener can do
oauth2.consent_redirect_resolveBefore redirecting to consent — override consentPath to route specific scopes to a custom consent page.
oauth2.deny_redirect_resolveWhen the user denies — override the redirect URL (e.g. send a payment scope back to its checkout).
oauth2.before_token_issuedBefore the default token is created — override the token pair, or return a bespoke HTTP response instead of an OAuth token.
oauth2.token_issuedAfter a token is issued for a request carrying state — observe the issued token, user, and scopes.

The module also exports its services and models (getOAuthClients, getTokenService, getAuthCodes) so other server-side modules can validate tokens or issue them directly.

Next Steps

  • Updates and Modules — Install and update the OAuth2 Server module
  • DashNex CLI — Manage environment variables and deployments
  • MCP — Expose your WebApp's API to AI agents