Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/actions/build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ inputs:
node:
description: The Node version to use
required: false
default: 18
default: 24

runs:
using: composite
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ jobs:
rl-scanner:
uses: ./.github/workflows/rl-secure.yml
with:
node-version: 18 ## depends if build requires node else we can remove this.
artifact-name: 'express-openid-connect.tgz' ## Will change respective to Repository
node-version: 24 ## depends if build requires node else we can remove this.
artifact-name: 'express-openid-connect.tgz' ## Will change respective to Repository
secrets:
RLSECURE_LICENSE: ${{ secrets.RLSECURE_LICENSE }}
RLSECURE_SITE_KEY: ${{ secrets.RLSECURE_SITE_KEY }}
Expand All @@ -32,7 +32,7 @@ jobs:
uses: ./.github/workflows/npm-release.yml
needs: rl-scanner ## this is important as this will not let release job to run until rl-scanner is done
with:
node-version: 18
node-version: 24
require-build: false
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}

env:
NODE_VERSION: 18
NODE_VERSION: 22
CACHE_KEY: '${{ github.ref }}-${{ github.run_id }}-${{ github.run_attempt }}'

jobs:
Expand Down
190 changes: 190 additions & 0 deletions V3_MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# V3 Migration Guide

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought.
Since consumer facing api has no breaking changes, do we need to mention that and it's usage examples here, in detail.
IMO, if consumer comes to read the migration guide, consumer has to go through a lot of detail to be assured that there are no breaking changes.
Would it not suffice to just mention the breaking changes and what consumer are required to follow.
@aks96 What do you think!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we are making sure user understand what changes has been done, regarding breaking changes it just user has to update node, openid-client and jose version.


`v3.x` upgrades the underlying OpenID Connect and JWT dependencies (`openid-client` v4 → v6, `jose` v2 → v6) to their latest major versions, bringing improved security, performance, and standards compliance.

**Important:** While this is a major version bump for the library, **there are ZERO breaking changes to the public API**. Your application code does not need to change.

---

**Public API:** No breaking changes - all configuration, middleware, and context APIs work exactly the same
**Node.js Version:** Now requires `^20.19.0 || ^22.12.0 || >= 23.0.0` (previously Node.js 14+)
**Module Support:** Works with BOTH CommonJS and ESM apps

---

## Breaking Changes

### Node.js Version Requirement

**The only breaking change is the specific Node.js version requirement.**

| Version | Minimum Node.js | Status |
| ------- | --------------------------------------- | ------- |
| v2.x | Node.js 14+ | Old |
| v3.x | `^20.19.0 \|\| ^22.12.0 \|\| >= 23.0.0` | **New** |

#### Why These Specific Versions?

The updated dependencies (`openid-client` v6 and `jose` v6) are **ESM-only packages**.

Node.js added `require(ESM)` support in:

- **v20.19.0** (backported to v20.x LTS)
- **v22.12.0** (backported to v22.x LTS)
- **v23.0.0+** (included by default)

There is **no workaround** - you must upgrade to a supported Node.js version.

#### Module System Support

**Works with BOTH CommonJS and ESM apps** - same Node.js requirements for both:

```javascript
// CommonJS - Works on supported Node.js versions
const { auth } = require('express-openid-connect');

// ESM - Works on supported Node.js versions
import { auth } from 'express-openid-connect';
```

**Note:** ESM apps need `"type": "module"` in `package.json` but have identical Node.js version requirements as CommonJS apps.

### Configuration

All configuration options work exactly as before. No changes needed.

```js
const { auth } = require('express-openid-connect');

// This configuration works EXACTLY the same in v3.x
app.use(
auth({
authRequired: false,
auth0Logout: true,
baseURL: 'https://example.com',
clientID: 'YOUR_CLIENT_ID',
issuerBaseURL: 'https://YOUR_DOMAIN',
secret: 'LONG_RANDOM_STRING',

// All these options still work
idpLogout: true,
idTokenSigningAlg: 'RS256',
clientAuthMethod: 'client_secret_post',
pushedAuthorizationRequests: true,
// ... etc
}),
);
```

### Middleware

All middleware functions work identically.

```js
const { auth, requiresAuth } = require('express-openid-connect');

// All these work EXACTLY the same
app.use(auth(config));
app.get('/admin', requiresAuth(), (req, res) => {
res.send('Admin page');
});
```

### Request Context (req.oidc)

The entire `req.oidc` API remains unchanged.

```js
// Before (v2.x)
app.get('/profile', async (req, res) => {
const user = req.oidc.user;
const claims = req.oidc.idTokenClaims;
const isAuthenticated = req.oidc.isAuthenticated();
const idToken = req.oidc.idToken;
const accessToken = req.oidc.accessToken;
const refreshToken = req.oidc.refreshToken;
const userInfo = await req.oidc.fetchUserInfo();

res.oidc.login({});
res.oidc.logout({});
});

// After (v3.x) - SAME
app.get('/profile', async (req, res) => {
const user = req.oidc.user;
const claims = req.oidc.idTokenClaims;
const isAuthenticated = req.oidc.isAuthenticated();
const idToken = req.oidc.idToken;
const accessToken = req.oidc.accessToken;
const refreshToken = req.oidc.refreshToken;
const userInfo = await req.oidc.fetchUserInfo();

res.oidc.login({});
res.oidc.logout({});
});
```

### Routes

Custom route configuration remains unchanged.

```js
// This works the same in v3.x
app.use(
auth({
routes: {
login: '/custom/login',
logout: '/custom/logout',
callback: '/custom/callback',
postLogoutRedirect: '/custom/post-logout',
},
}),
);
```

### Session Handling

Session configuration, custom stores, and lifecycle hooks all work the same.

```js
// Session configuration - UNCHANGED
app.use(
auth({
session: {
rolling: true,
rollingDuration: 86400,
absoluteDuration: 86400 * 7,
store: customStore, // Custom session stores still work
},
}),
);
```

### Authentication Methods

All client authentication methods continue to work:

```js
// All these still work in v3.x
const config = {
clientAuthMethod: 'client_secret_basic',
clientAuthMethod: 'client_secret_post',
clientAuthMethod: 'client_secret_jwt',
clientAuthMethod: 'private_key_jwt',
clientAuthMethod: 'none',
};
```

---

```bash
node --version
```

**Required:** `v20.19.0+`, `v22.12.0+`, or `v23.0.0+`

If your version is older:

- **v20.0.0 - v20.18.0** → Upgrade to v20.19.0+
- **v22.0.0 - v22.11.0** → Upgrade to v22.12.0+
- **v18.x or earlier** → Upgrade to v22.12.0+ (recommended LTS)
36 changes: 18 additions & 18 deletions end-to-end/fixture/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const path = require('path');
const crypto = require('crypto');
const sinon = require('sinon');
const express = require('express');
const { JWT } = require('jose');
const jose = require('jose');
const { privateJWK } = require('./jwk');
const request = require('request-promise-native').defaults({ json: true });

Expand Down Expand Up @@ -93,24 +93,24 @@ const logout = async (page) => {
};

const logoutTokenTester = (clientId, sid, sub) => async (req, res) => {
const logoutToken = JWT.sign(
{
events: {
'http://schemas.openid.net/event/backchannel-logout': {},
},
...(sid && { sid: req.oidc.user.sid }),
...(sub && { sub: req.oidc.user.sub }),
},
privateJWK,
{
issuer: `http://localhost:${process.env.PROVIDER_PORT || 3001}`,
audience: clientId,
iat: true,
jti: crypto.randomBytes(16).toString('hex'),
algorithm: 'RS256',
header: { typ: 'logout+jwt' },
// Create private key from JWK for signing
const privateKey = await jose.importJWK(privateJWK, 'RS256');

const claims = {
events: {
'http://schemas.openid.net/event/backchannel-logout': {},
},
);
...(sid && { sid: req.oidc.user.sid }),
...(sub && { sub: req.oidc.user.sub }),
};

const logoutToken = await new jose.SignJWT(claims)
.setProtectedHeader({ alg: 'RS256', typ: 'logout+jwt' })
.setIssuer(`http://localhost:${process.env.PROVIDER_PORT || 3001}`)
.setAudience(clientId)
.setIssuedAt()
.setJti(crypto.randomBytes(16).toString('hex'))
.sign(privateKey);

res.send(`
<pre style="border: 1px solid #ccc; padding: 10px; white-space: break-spaces; background: whitesmoke;">curl -X POST http://localhost:3000/backchannel-logout -d "logout_token=${logoutToken}"</pre>
Expand Down
103 changes: 96 additions & 7 deletions end-to-end/fixture/jwk.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,101 @@
const { JWK } = require('jose');
/**
* ⚠️ WARNING: TEST KEYS ONLY - DO NOT USE IN PRODUCTION ⚠️
*
* This file contains RSA private keys that are INTENTIONALLY PUBLIC for testing.
* These keys are:
* - FOR TESTING ONLY - Never use in production
* - PUBLICLY COMMITTED - Not secret, safe for test fixtures
* - DETERMINISTIC - Same keys every test run for reproducibility
*
* Pre-generated RSA-2048 key pair for testing.
*
* Note: jose v6 only provides async key generation (generateKeyPair), which cannot
* be used at module load time. Using a static key here is preferable for tests as it:
* - Ensures deterministic test results
* - Improves test performance (no generation overhead)
* - Simplifies debugging with consistent test data
*
* This key was originally sourced from examples/private-key.pem and matches the
* private key JWT example in the codebase.
*
* See end-to-end/fixture/README.md for more information on test key security.
*
* To regenerate if needed:
* ```
* const { generateKeyPair, exportPKCS8, exportSPKI, exportJWK } = require('jose');
* const { publicKey, privateKey } = await generateKeyPair('RS256', { modulusLength: 2048 });
* const privatePEM = await exportPKCS8(privateKey);
* const publicPEM = await exportSPKI(publicKey);
* const privateJWK = await exportJWK(privateKey);
* const publicJWK = await exportJWK(publicKey);
* ```
*/

const key = JWK.generateSync('RSA', 2048, {
const privatePEM = `-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDbTKOQLtaZ6U1k
3fcYCMVoy8poieNPPcbj15TCLOm4Bbox73/UUxIArqczVcjtUGnL+jn5982V5EiB
y8W51m5K9mIBgEFLYdLkXk+OW5UTE/AdMPtfsIjConGrrs3mxN4WSH9kvh9Yr41r
hWUUSwqFyMOssbGE8K46Cv0WYvS7RXH9MzcyTcMSFp/60yUXH4rdHYZElF7XCdiE
63WxebxI1Qza4xkjTlbp5EWfWBQB1Ms10JO8NjrtkCXrDI57Bij5YanPAVhctcO9
z5/y9i5xEzcer8ZLO8VDiXSdEsuP/fe+UKDyYHUITD8u51p3O2JwCKvdTHduemej
3Kd1RlHrAgMBAAECggEATWdzpASkQpcSdjPSb21JIIAt5VAmJ2YKuYjyPMdVh1qe
Kdn7KJpZlFwRMBFrZjgn35Nmu1A4BFwbK5UdKUcCjvsABL+cTFsu8ORI+Fpi9+Tl
r6gGUfQhkXF85bhBfN6n9P2J2akxrz/njrf6wXrrL+V5C498tQuus1YFls0+zIpD
N+GngNOPHlGeY3gW4K/HjGuHwuJOvWNmE4KNQhBijdd50Am824Y4NV/SmsIo7z+s
8CLjp/qtihwnE4rkUHnR6M4u5lpzXOnodzkDTG8euOJds0T8DwLNTx1b+ETim35i
D/hOCVwl8QFoj2aatjuJ5LXZtZUEpGpBF2TQecB+gQKBgQDvaZ1jG/FNPnKdayYv
z5yTOhKM6JTB+WjB0GSx8rebtbFppiHGgVhOd1bLIzli9uMOPdCNuXh7CKzIgSA6
Q76Wxfuaw8F6CBIdlG9bZNL6x8wp6zF8tGz/BgW7fFKBwFYSWzTcStGr2QGtwr6F
9p1gYPSGfdERGOQc7RmhoNNHcQKBgQDqfkhpPfJlP/SdFnF7DDUvuMnaswzUsM6D
ZPhvfzdMBV8jGc0WjCW2Vd3pvsdPgWXZqAKjN7+A5HiT/8qv5ruoqOJSR9ZFZI/B
8v+8gS9Af7K56mCuCFKZmOXUmaL+3J2FKtzAyOlSLjEYyLuCgmhEA9Zo+duGR5xX
AIjx7N/ZGwKBgCZAYqQeJ8ymqJtcLkq/Sg3/3kzjMDlZxxIIYL5JwGpBemod4BGe
QuSujpCAPUABoD97QuIR+xz1Qt36O5LzlfTzBwMwOa5ssbBGMhCRKGBnIcikylBZ
Z3zLkojlES2n9FiUd/qmfZ+OWYVQsy4mO/jVJNyEJ64qou+4NjsrvfYRAoGAORki
3K1+1nSqRY3vd/zS/pnKXPx4RVoADzKI4+1gM5yjO9LOg40AqdNiw8X2lj9143fr
nH64nNQFIFSKsCZIz5q/8TUY0bDY6GsZJnd2YAg4JtkRTY8tPcVjQU9fxxtFJ+X1
9uN1HNOulNBcCD1k0hr1HH6qm5nYUb8JmY8KOr0CgYB85pvPhBqqfcWi6qaVQtK1
ukIdiJtMNPwePfsT/2KqrbnftQnAKNnhsgcYGo8NAvntX4FokOAEdunyYmm85mLp
BGKYgVXJqnm6+TJyCRac1ro3noG898P/LZ8MOBoaYQtWeWRpDc46jPrA0FqUJy+i
ca/T0LLtgmbMmxSv/MmzIg==
-----END PRIVATE KEY-----`;

const publicPEM = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA20yjkC7WmelNZN33GAjF
aMvKaInjTz3G49eUwizpuAW6Me9/1FMSAK6nM1XI7VBpy/o5+ffNleRIgcvFudZu
SvZiAYBBS2HS5F5PjluVExPwHTD7X7CIwqJxq67N5sTeFkh/ZL4fWK+Na4VlFEsK
hcjDrLGxhPCuOgr9FmL0u0Vx/TM3Mk3DEhaf+tMlFx+K3R2GRJRe1wnYhOt1sXm8
SNUM2uMZI05W6eRFn1gUAdTLNdCTvDY67ZAl6wyOewYo+WGpzwFYXLXDvc+f8vYu
cRM3Hq/GSzvFQ4l0nRLLj/33vlCg8mB1CEw/LudadzticAir3Ux3bnpno9yndUZR
6wIDAQAB
-----END PUBLIC KEY-----`;

// For JWK format
const privateJWK = {
kty: 'RSA',
kid: 'key-1',
use: 'sig',
alg: 'RS256',
n: '20yjkC7WmelNZN33GAjFaMvKaInjTz3G49eUwizpuAW6Me9_1FMSAK6nM1XI7VBpy_o5-ffNleRIgcvFudZuSvZiAYBBS2HS5F5PjluVExPwHTD7X7CIwqJxq67N5sTeFkh_ZL4fWK-Na4VlFEsKhcjDrLGxhPCuOgr9FmL0u0Vx_TM3Mk3DEhaf-tMlFx-K3R2GRJRe1wnYhOt1sXm8SNUM2uMZI05W6eRFn1gUAdTLNdCTvDY67ZAl6wyOewYo-WGpzwFYXLXDvc-f8vYucRM3Hq_GSzvFQ4l0nRLLj_33vlCg8mB1CEw_LudadzticAir3Ux3bnpno9yndUZR6w',
e: 'AQAB',
d: 'TWdzpASkQpcSdjPSb21JIIAt5VAmJ2YKuYjyPMdVh1qeKdn7KJpZlFwRMBFrZjgn35Nmu1A4BFwbK5UdKUcCjvsABL-cTFsu8ORI-Fpi9-Tlr6gGUfQhkXF85bhBfN6n9P2J2akxrz_njrf6wXrrL-V5C498tQuus1YFls0-zIpDN-GngNOPHlGeY3gW4K_HjGuHwuJOvWNmE4KNQhBijdd50Am824Y4NV_SmsIo7z-s8CLjp_qtihwnE4rkUHnR6M4u5lpzXOnodzkDTG8euOJds0T8DwLNTx1b-ETim35iD_hOCVwl8QFoj2aatjuJ5LXZtZUEpGpBF2TQecB-gQ',
p: '72mdYxvxTT5ynWsmL8-ckzoSjOiUwflowdBksfK3m7WxaaYhxoFYTndWyyM5YvbjDj3Qjbl4ewisyIEgOkO-lsX7msPBeggSHZRvW2TS-sfMKesxfLRs_wYFu3xSgcBWEls03ErRq9kBrcK-hfadYGD0hn3RERjkHO0ZoaDTR3E',
q: '6n5IaT3yZT_0nRZxeww1L7jJ2rMM1LDOg2T4b383TAVfIxnNFowltlXd6b7HT4Fl2agCoze_gOR4k__Kr-a7qKjiUkfWRWSPwfL_vIEvQH-yuepgrghSmZjl1Jmi_tydhSrcwMjpUi4xGMi7goJoRAPWaPnbhkecVwCI8ezf2Rs',
dp: 'JkBipB4nzKaom1wuSr9KDf_eTOMwOVnHEghgvknAakF6ah3gEZ5C5K6OkIA9QAGgP3tC4hH7HPVC3fo7kvOV9PMHAzA5rmyxsEYyEJEoYGchyKTKUFlnfMuSiOURLaf0WJR3-qZ9n45ZhVCzLiY7-NUk3IQnriqi77g2Oyu99hE',
dq: 'ORki3K1-1nSqRY3vd_zS_pnKXPx4RVoADzKI4-1gM5yjO9LOg40AqdNiw8X2lj9143frnH64nNQFIFSKsCZIz5q_8TUY0bDY6GsZJnd2YAg4JtkRTY8tPcVjQU9fxxtFJ-X19uN1HNOulNBcCD1k0hr1HH6qm5nYUb8JmY8KOr0',
qi: 'fOabz4Qaqn3FouqmlULStbpCHYibTDT8Hj37E_9iqq2537UJwCjZ4bIHGBqPDQL57V-BaJDgBHbp8mJpvOZi6QRimIFVyap5uvkycgkWnNa6N56BvPfD_y2fDDgaGmELVnlkaQ3OOoz6wNBalCcvonGv09Cy7YJmzJsUr_zJsyI',
};

const publicJWK = {
kty: 'RSA',
kid: 'key-1',
use: 'sig',
});
alg: 'RS256',
n: '20yjkC7WmelNZN33GAjFaMvKaInjTz3G49eUwizpuAW6Me9_1FMSAK6nM1XI7VBpy_o5-ffNleRIgcvFudZuSvZiAYBBS2HS5F5PjluVExPwHTD7X7CIwqJxq67N5sTeFkh_ZL4fWK-Na4VlFEsKhcjDrLGxhPCuOgr9FmL0u0Vx_TM3Mk3DEhaf-tMlFx-K3R2GRJRe1wnYhOt1sXm8SNUM2uMZI05W6eRFn1gUAdTLNdCTvDY67ZAl6wyOewYo-WGpzwFYXLXDvc-f8vYucRM3Hq_GSzvFQ4l0nRLLj_33vlCg8mB1CEw_LudadzticAir3Ux3bnpno9yndUZR6w',
e: 'AQAB',
};

module.exports.privateJWK = key.toJWK(true);
module.exports.publicJWK = key.toJWK();
module.exports.privatePEM = key.toPEM(true);
module.exports.publicPEM = key.toPEM();
module.exports.privateJWK = privateJWK;
module.exports.publicJWK = publicJWK;
module.exports.privatePEM = privatePEM;
module.exports.publicPEM = publicPEM;
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = [
{
...js.configs.recommended,
languageOptions: {
ecmaVersion: 2019,
ecmaVersion: 2020,
globals: {
...require('globals').node,
...require('globals').es6,
Expand Down
Loading