OAuth2 Server for Node.js
oauth2-server is a complete, framework-agnostic, and well-tested module for implementing an OAuth2 Authorization Server in Node.js. It adheres to RFC 6749 (OAuth 2.0 Authorization Framework) and RFC 6750 (Bearer Token Usage), providing the core logic for handling various OAuth 2.0 grant types including `authorization_code`, `client_credentials`, `refresh_token`, and `password` grants, as well as support for custom extension grants and scopes. The library is currently at version 3.1.1 and is under active maintenance, with recent releases focusing on bug fixes and dependency updates after a period of hiatus. It distinguishes itself by offering a robust, compliant foundation that can be integrated with any Node.js HTTP framework (like Express or Koa via official wrappers), supporting promises, Node-style callbacks, and async/await for model interactions. It doesn't dictate a specific storage mechanism, allowing developers to plug in their preferred database (e.g., PostgreSQL, MongoDB, Redis).
Common errors
-
Error: model must implement getAccessToken()
cause The configured OAuth2Server model object is missing a required method, `getAccessToken`, or it's not correctly defined. This method is fundamental for authenticating bearer tokens.fixEnsure your `model` object passed to `new OAuth2Server()` includes an asynchronous function `getAccessToken(accessToken)` that returns a token object if valid, or `null` otherwise. Review the model specification for all required methods based on your enabled grants. -
OAuth2Error: invalid_grant (The provided authorization grant is invalid, expired, revoked, or was issued to another client.)
cause This error often occurs when an authorization code is used more than once, has expired, or was issued to a different client than the one attempting to exchange it. It can also happen if the refresh token is invalid or revoked.fixFor authorization codes, ensure they are single-use and quickly invalidated after first use. Verify expiry times for all tokens. Double-check that the `client_id` and `client_secret` used for token exchange match the client that initiated the authorization flow. Inspect logs for more specific reasons (e.g., 'code expired'). -
OAuth2Error: redirect_uri_mismatch (The redirection URI provided does not match a pre-registered redirection URI for the application.)
cause The `redirect_uri` sent by the client in the authorization request does not exactly match one of the `redirectUris` configured for that client in your OAuth2 server's model. Even subtle differences, like a trailing slash or case, can cause this.fixEnsure the `redirect_uri` parameter in the client's authorization request is an exact string match for one of the `redirectUris` associated with the `client_id` in your OAuth2 model. All valid `redirectUris` should be explicitly listed and strictly validated. -
ERR_REQUIRE_ESM: require() of ES Module ... not supported.
cause You are attempting to use the CommonJS `require()` syntax to import `oauth2-server` (or its wrappers) in a Node.js project configured for ES Modules (`"type": "module"` in `package.json`), or vice versa, in a way that Node.js cannot reconcile.fixIf your project is ES Module-based, use `import OAuth2Server from 'oauth2-server';`. If your project is CommonJS, use `const OAuth2Server = require('oauth2-server');`. Ensure your `package.json`'s `type` field and module syntax are consistent. Consider using a bundler like Webpack or Rollup for complex module environments.
Warnings
- breaking Migrating from `oauth2-server` 2.x to 3.x involves significant breaking changes. Several server options, such as `grants`, `debug`, `clientIdRegex`, `passthroughErrors`, and `continueAfterResponse`, have been removed. The model specification has also changed, requiring updates to your implementation of methods like `getAccessToken` and `generateAccessToken`. The module now primarily leverages Promises for asynchronous operations, though it retains compatibility with Node-style callbacks, ES6 generators, and async/await.
- gotcha While `oauth2-server` provides a robust implementation of OAuth 2.0 (RFC 6749/6750), the newer OAuth 2.1 standard (a consolidation of security best practices) deprecates several older, insecure flows and introduces new requirements. Notably, the Implicit Grant and Resource Owner Password Credentials (ROPC) flows are removed, and Proof Key for Code Exchange (PKCE) is mandatory for all public clients. OAuth 2.1 also strictly prohibits passing bearer tokens in URL query parameters and requires exact matching for redirect URIs.
- gotcha Improper validation of `redirect_uri` can lead to critical security vulnerabilities, including authorization code or token leakage, allowing attackers to hijack user accounts. Overly permissive redirect URIs (e.g., using wildcards or prefix matching) or typos can be exploited.
- gotcha The `state` parameter in OAuth 2.0 is crucial for Cross-Site Request Forgery (CSRF) protection. If it's missing, weak (predictable), or not properly validated by the client application, it creates an attack vector where an attacker can complete the authorization flow on behalf of a victim.
- gotcha Misconfigurations within the `model` object, which defines how `oauth2-server` interacts with your data store, are a common source of runtime errors and incorrect behavior. Forgetting to implement a required method (e.g., `getClient`, `saveToken`) or returning an invalid data structure can lead to `invalid_request` or `server_error` responses.
Install
-
npm install oauth2-server -
yarn add oauth2-server -
pnpm add oauth2-server
Imports
- OAuth2Server
const OAuth2Server = require('oauth2-server').OAuth2Server;import OAuth2Server from 'oauth2-server';
- Request, Response
import { OAuth2Request, OAuth2Response } from 'oauth2-server';import { Request, Response } from 'oauth2-server'; - express-oauth-server (wrapper)
import OAuth2Server from 'oauth2-server'; // Directly with Express
import OAuth2Server from '@oauthjs/express-oauth-server';
Quickstart
import express from 'express';
import OAuth2Server, { Request, Response } from 'oauth2-server';
import bodyParser from 'body-parser';
// A minimalistic in-memory model for demonstration purposes.
const model = {
// Client storage
clients: [{
id: 'client1',
secret: 'clientsecret',
redirectUris: ['http://localhost:3000/oauth/callback'],
grants: ['authorization_code', 'refresh_token', 'password', 'client_credentials'],
}],
// Token/Code storage
tokens: [],
authorizationCodes: [],
// Required: getClient(clientId, clientSecret, callback)
async getClient(clientId, clientSecret) {
const client = this.clients.find(c => c.id === clientId);
if (!client) return null;
if (clientSecret && client.secret !== clientSecret) return null;
return client;
},
// Required for authorization_code grant
async saveAuthorizationCode(code, client, user) {
this.authorizationCodes.push({ code: code.authorizationCode, expiresAt: code.expiresAt, redirectUri: code.redirectUri, scope: code.scope, client: client, user: user });
return code;
},
async getAuthorizationCode(authorizationCode) {
const code = this.authorizationCodes.find(c => c.code === authorizationCode && c.expiresAt > new Date());
if (!code) return null;
// In a real app, delete the code after use: this.authorizationCodes = this.authorizationCodes.filter(c => c.code !== authorizationCode);
return code;
},
async revokeAuthorizationCode(code) {
this.authorizationCodes = this.authorizationCodes.filter(c => c.code !== code.authorizationCode);
return true;
},
// Required for password, client_credentials, refresh_token grants
async saveToken(token, client, user) {
this.tokens.push({ accessToken: token.accessToken, accessTokenExpiresAt: token.accessTokenExpiresAt, refreshToken: token.refreshToken, refreshTokenExpiresAt: token.refreshTokenExpiresAt, scope: token.scope, client: client, user: user });
return token;
},
async getAccessToken(accessToken) {
return this.tokens.find(t => t.accessToken === accessToken && t.accessTokenExpiresAt > new Date());
},
async getRefreshToken(refreshToken) {
return this.tokens.find(t => t.refreshToken === refreshToken && t.refreshTokenExpiresAt > new Date());
},
async revokeToken(token) {
this.tokens = this.tokens.filter(t => t.refreshToken !== token.refreshToken);
return true;
},
// Required for password grant
async getUser(username, password) {
// Dummy user for 'password' grant
if (username === 'testuser' && password === 'testpass') {
return { id: '123', username: 'testuser' };
}
return null;
},
// Optional: verifyScope(token, scope)
async verifyScope(token, scope) {
if (!scope) return true;
const requestedScopes = scope.split(' ');
const authorizedScopes = token.scope.split(' ');
return requestedScopes.every(s => authorizedScopes.includes(s));
}
};
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
const oauth = new OAuth2Server({
model: model,
accessTokenLifetime: 60 * 60, // 1 hour
allowBearerTokensInQueryString: true, // Only for testing, not recommended for production.
});
// Grant token endpoint (e.g., /oauth/token for password, client_credentials, refresh_token grants)
app.post('/oauth/token', async (req, res) => {
try {
const token = await oauth.token(new Request(req), new Response(res));
res.json(token);
} catch (err) {
console.error(err);
res.status(err.code || 500).json(err);
}
});
// Authorize endpoint (e.g., /oauth/authorize for authorization_code grant)
app.get('/oauth/authorize', async (req, res) => {
// In a real application, render an authorization screen here
// For this example, we'll auto-approve if 'user' query param is present
if (req.query.response_type === 'code' && req.query.client_id && req.query.redirect_uri) {
if (req.query.user === 'approved') {
const authRequest = new Request(req);
const authResponse = new Response(res);
try {
const code = await oauth.authorize(authRequest, authResponse, {
authenticateHandler: { handle: async () => ({ id: '123', username: 'testuser' }) }
});
res.redirect(code.redirectUri + '?code=' + code.authorizationCode);
} catch (err) {
console.error(err);
res.status(err.code || 500).json(err);
}
} else {
// Simulate a login/consent page redirect
res.send(`
<h1>Authorize Client</h1>
<p>Client ${req.query.client_id} wants access to your data.</p>
<form action="/oauth/authorize" method="GET">
<input type="hidden" name="response_type" value="${req.query.response_type}">
<input type="hidden" name="client_id" value="${req.query.client_id}">
<input type="hidden" name="redirect_uri" value="${req.query.redirect_uri}">
<input type="hidden" name="scope" value="${req.query.scope || ''}">
<button type="submit" name="user" value="approved">Approve</button>
</form>
`);
}
} else {
res.status(400).json({ error: 'invalid_request', error_description: 'Missing required parameters for authorization.' });
}
});
// Protected resource endpoint
app.get('/protected', async (req, res) => {
try {
const token = await oauth.authenticate(new Request(req), new Response(res));
res.json({ message: 'Hello, ' + token.user.username + '!', tokenInfo: token });
} catch (err) {
console.error(err);
res.status(err.code || 500).json(err);
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`OAuth2 server listening on port ${PORT}`);
console.log('Try POST to /oauth/token with grant_type=password, username=testuser, password=testpass, client_id=client1, client_secret=clientsecret');
console.log('Try GET to /oauth/authorize with ?response_type=code&client_id=client1&redirect_uri=http://localhost:3000/oauth/callback&scope=read');
console.log('Then navigate to /protected with Authorization: Bearer <access_token>');
});