Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9,674 changes: 3,477 additions & 6,197 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions packages/examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"build": "exit 0",
"dev": "tsnd index.ts"
},
"dependencies": {
"express-oauth2-jwt-bearer": "file:../express-oauth2-jwt-bearer",
"express-rate-limit": "^8.2.1"
},
"devDependencies": {
"@tsconfig/node12": "^1.0.11",
"@types/cors": "^2.8.13",
Expand All @@ -29,6 +33,7 @@
"ts-jest": "^29.0.5",
"ts-node-dev": "^2.0.0",
"tslib": "^2.5.0",
"tsx": "^4.20.6",
"typescript": "^5.0.2"
},
"engines": {
Expand Down
213 changes: 213 additions & 0 deletions packages/examples/token-exchange-example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/**
* Comprehensive Token Exchange Examples
*
* This file demonstrates both approaches for token exchange:
* 1. Direct exchangeToken() function - for manual token exchange
* 2. req.auth.exchange() method - for Express middleware integration
*/

import express from 'express';
import { auth, exchangeToken } from 'express-oauth2-jwt-bearer';
import rateLimit from 'express-rate-limit';

const app = express();
app.use(express.json());

// Rate limiter: max 100 requests per 15 minutes per IP for expensive routes
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});

// Setup the auth middleware
const authenticateToken = auth({
issuerBaseURL: 'https://dev-ankita-t.us.auth0.com',
audience: 'https://api.example.com'
});

// Health check endpoint (no authentication required)
app.get('/health', (req, res) => {
res.json({ status: 'OK', message: 'Token exchange service is running' });
});

// Example 1: Exchange using request auth context (recommended for Express middleware)
// This approach automatically uses the token from the current authenticated request
app.post('/exchange-via-context', authenticateToken, limiter, async (req, res) => {
try {
if (!req.auth) {
return res.status(401).json({ error: 'Authentication required' });
}

const exchangedToken = await req.auth.exchange({
tokenEndpoint: 'https://dev-ankita-t.us.auth0.com/oauth/token',
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
targetAudience: 'https://api.example.com',
scope: 'read:data write:data',
});

res.json({
message: 'Token exchanged successfully via auth context',
exchangedToken,
});
} catch (error) {
console.error('Token exchange failed:', error);
res.status(500).json({
error: 'Token exchange failed',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
});

// Example 2: Direct token exchange using the exchangeToken function
// This approach allows you to exchange any token manually
app.post('/exchange-direct', authenticateToken, limiter, async (req, res) => {
try {
if (!req.auth) {
return res.status(401).json({ error: 'Authentication required' });
}

// Get the current token from the authenticated request
const currentToken = req.auth.token;

// Use the direct exchangeToken function - useful for custom scenarios
const exchangedToken = await exchangeToken(currentToken, {
tokenEndpoint: 'https://dev-ankita-t.us.auth0.com/oauth/token',
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
targetAudience: 'https://api.example.com',
scope: 'read:data write:data',
});

res.json({
message: 'Token exchanged successfully via direct function',
exchangedToken,
});
} catch (error) {
console.error('Token exchange failed:', error);
res.status(500).json({
error: 'Token exchange failed',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
});

// Example 3: Exchange a token from request body (demonstrating flexibility of direct function)
// This shows how you can exchange any token, not just the current request's token
app.post('/exchange-any-token', async (req, res) => {
try {
const { token } = req.body;

if (!token) {
return res.status(400).json({ error: 'Token is required in request body' });
}

// Use exchangeToken to exchange any provided token
const exchangedToken = await exchangeToken(token, {
tokenEndpoint: 'https://dev-ankita-t.us.auth0.com/oauth/token',
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
targetAudience: 'https://api.example.com',
scope: 'read:data write:data',
});

res.json({
message: 'External token exchanged successfully',
exchangedToken,
});
} catch (error) {
console.error('Token exchange failed:', error);
res.status(500).json({
error: 'Token exchange failed',
details: error instanceof Error ? error.message : 'Unknown error',
});
}
});

// Demonstration endpoint showing the difference
app.get('/compare-approaches', authenticateToken, async (req, res) => {
if (!req.auth) {
return res.status(401).json({ error: 'Authentication required' });
}

res.json({
approaches: {
authContext: {
description: 'Uses req.auth.exchange() - automatically uses current request token',
pros: [
'Simpler API - no need to extract token manually',
'Follows express-openid-connect pattern',
'Integrated with Express middleware',
'Less error-prone'
],
useCase: 'Best for typical Express apps where you want to exchange the current user\'s token'
},
directFunction: {
description: 'Uses exchangeToken(token, options) - manual token exchange',
pros: [
'More flexible - can exchange any token',
'Can be used outside Express middleware context',
'Useful for batch processing or admin operations',
'Direct control over which token to exchange'
],
useCase: 'Best for custom scenarios, background jobs, or when you need to exchange tokens from different sources'
}
},
examples: {
authContext: 'POST /exchange-via-context (with Authorization header)',
directFunction: 'POST /exchange-direct (with Authorization header) or POST /exchange-any-token (with token in body)'
}
});
});

// Information endpoint for getting tokens (for testing)
app.get('/get-token', (req, res) => {
res.json({
message: 'To get tokens for testing, use one of these approaches:',
approaches: {
spa: {
description: 'For Single Page Applications',
flow: 'Authorization Code with PKCE',
steps: [
'1. Configure your Auth0 application as "Single Page Application"',
'2. Use Auth0 SDK or direct OAuth flow with PKCE',
'3. Redirect user to authorize endpoint',
'4. Exchange authorization code for tokens'
],
authUrl: 'https://dev-ankita-t.us.auth0.com/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_CALLBACK&scope=openid%20profile&audience=https://api.example.com&code_challenge=YOUR_CODE_CHALLENGE&code_challenge_method=S256'
},
m2m: {
description: 'For Machine-to-Machine Applications',
flow: 'Client Credentials',
note: 'Enable "Client Credentials" grant in Auth0 Dashboard > Applications > Your App > Settings > Advanced Settings > Grant Types',
curl: `curl -X POST https://dev-ankita-t.us.auth0.com/oauth/token \\
-H "Content-Type: application/json" \\
-d '{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"audience": "https://api.example.com",
"grant_type": "client_credentials"
}'`
}
}
});
});

const port = process.env.PORT || 3006;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
console.log('');
console.log('Available endpoints:');
console.log(' GET /health - Health check (no auth)');
console.log(' GET /get-token - Token acquisition info (no auth)');
console.log(' GET /compare-approaches - Compare token exchange methods (requires auth)');
console.log(' POST /exchange-via-context - Exchange via req.auth.exchange() (requires auth)');
console.log(' POST /exchange-direct - Exchange via exchangeToken() function (requires auth)');
console.log(' POST /exchange-any-token - Exchange any token (no auth, token in body)');
console.log('');
console.log('Key Differences:');
console.log(' • req.auth.exchange() - Automatic, uses current request token');
console.log(' • exchangeToken() - Manual, can exchange any token');
});
104 changes: 104 additions & 0 deletions packages/express-oauth2-jwt-bearer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,106 @@ To learn how to:

**See [DPoP examples in `EXAMPLES.md`](https://github.com/auth0/node-oauth2-jwt-bearer/blob/main/packages/express-oauth2-jwt-bearer/EXAMPLES.md#dpop-authentication-early-access)**

### Token Exchange (OAuth 2.0 RFC 8693)

This SDK supports [OAuth 2.0 Token Exchange (RFC 8693)](https://datatracker.ietf.org/doc/html/rfc8693), which allows you to exchange an access token for a different token, typically for calling downstream APIs with different audiences or scopes.

#### Basic Token Exchange

```js
const { auth, tokenExchange } = require('express-oauth2-jwt-bearer');

// Setup auth middleware first
app.use(auth({
audience: 'https://api.example.com',
issuerBaseURL: 'https://auth.example.com',
}));

// Add token exchange middleware
app.use(tokenExchange({
tokenEndpoint: 'https://auth.example.com/oauth/token',
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
targetAudience: 'https://downstream-api.example.com',
}));

app.get('/api/data', async (req, res) => {
// Access the exchanged token
const exchangedToken = req.exchangedAuth?.access_token;

// Use it to call downstream APIs
const response = await fetch('https://downstream-api.example.com/data', {
headers: { 'Authorization': `Bearer ${exchangedToken}` }
});

const data = await response.json();
res.json(data);
});
```

#### Environment Variable Configuration

```shell
TOKEN_EXCHANGE_ENDPOINT=https://auth.example.com/oauth/token
TOKEN_EXCHANGE_CLIENT_ID=your-client-id
TOKEN_EXCHANGE_CLIENT_SECRET=your-client-secret
TOKEN_EXCHANGE_AUDIENCE=https://downstream-api.example.com
```

```js
// Uses environment variables
app.use(tokenExchange());
```

#### Conditional Token Exchange

```js
app.use(tokenExchange({
tokenEndpoint: process.env.TOKEN_EXCHANGE_ENDPOINT,
clientId: process.env.TOKEN_EXCHANGE_CLIENT_ID,
clientSecret: process.env.TOKEN_EXCHANGE_CLIENT_SECRET,
targetAudience: 'https://special-api.example.com',
when: 'conditional',
shouldExchange: (req) => req.path.startsWith('/api/special'),
onError: 'skip', // Continue even if exchange fails
}));
```

#### Manual Token Exchange

```js
const { exchangeToken } = require('express-oauth2-jwt-bearer');

app.get('/api/manual', async (req, res) => {
try {
const exchangedToken = await exchangeToken(req.auth.token, {
tokenEndpoint: 'https://auth.example.com/oauth/token',
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
targetAudience: 'https://special-api.example.com',
scope: 'read:special-data',
});

res.json({ success: true, tokenType: exchangedToken.token_type });
} catch (error) {
res.status(500).json({ error: 'Token exchange failed' });
}
});
```

After successful token exchange, your request object will have:

```js
app.get('/api/protected', (req, res) => {
const auth = req.auth; // Original token validation result
const exchangedAuth = req.exchangedAuth; // Exchanged token result

exchangedAuth.access_token; // The new access token
exchangedAuth.token_type; // Usually "Bearer"
exchangedAuth.expires_in; // Token expiration (seconds)
exchangedAuth.scope; // Token scope
});
```

### Security Headers

Expand All @@ -121,6 +221,10 @@ See the Express.js [docs on error handling](https://expressjs.com/en/guide/error

- [auth](https://auth0.github.io/node-oauth2-jwt-bearer/functions/auth.html) - Middleware that will return a 401 if a valid Access token JWT bearer token is not provided in the request.
- [AuthResult](https://auth0.github.io/node-oauth2-jwt-bearer/interfaces/AuthResult.html) - The properties added to `req.auth` upon successful authorization.
- [tokenExchange](./src/token-exchange.ts) - Middleware for OAuth 2.0 Token Exchange (RFC 8693) that exchanges access tokens for downstream API calls.
- [exchangeToken](./src/token-exchange.ts) - Function to manually perform OAuth 2.0 Token Exchange.
- [TokenExchangeOptions](./src/token-exchange.ts) - Configuration options for token exchange.
- [TokenExchangeResult](./src/token-exchange.ts) - The result of a successful token exchange.
- [requiredScopes](https://auth0.github.io/node-oauth2-jwt-bearer/functions/requiredScopes.html) - Check a token's scope claim to include a number of given scopes, raises a 403 `insufficient_scope` error if the value of the scope claim does not include all the given scopes.
- [claimEquals](https://auth0.github.io/node-oauth2-jwt-bearer/functions/claimEquals.html) - Check a token's claim to be equal a given JSONPrimitive (string, number, boolean or null) raises a 401 `invalid_token` error if the value of the claim does not match.
- [claimIncludes](https://auth0.github.io/node-oauth2-jwt-bearer/functions/claimIncludes.html) - Check a token's claim to include a number of given JSONPrimitives (string, number, boolean or null) raises a 401 `invalid_token` error if the value of the claim does not include all the given values.
Expand Down
2 changes: 2 additions & 0 deletions packages/express-oauth2-jwt-bearer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
"typescript": "^5.0.2"
},
"dependencies": {
"access-token-jwt": "0.0.1",
"oauth2-bearer": "0.0.1",
"jose": "^4.15.5"
},
"engines": {
Expand Down
Loading
Loading