Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6302c75
migration: openid-client and jose migration
aks96 Feb 9, 2026
fd04c9e
migration guide
aks96 Feb 9, 2026
27963f2
version fix and migration update
aks96 Feb 9, 2026
4e95839
CI fix
aks96 Feb 9, 2026
9c63bd8
CI fix
aks96 Feb 9, 2026
3d99deb
Migration guide
aks96 Feb 9, 2026
56b8799
addressed comments
aks96 Apr 7, 2026
796d59f
Merge branch 'master' into migration/openid-clientv6
aks96 Apr 7, 2026
51ad64c
fix failed actions
aks96 Apr 7, 2026
f1db455
fix failed actions
aks96 Apr 7, 2026
119567f
Merge remote-tracking branch 'origin/master' into migration/openid-cl…
aks96 Apr 17, 2026
22a7bcb
comments addressed
aks96 Apr 17, 2026
bfe775e
fixed edge cases
aks96 Apr 27, 2026
a4f79e0
construct callbackUrl explicitly from SDK's configured redirectUri to…
cschetan77 May 3, 2026
0318dea
Add debug if headers were already sent and session cookie could not b…
cschetan77 May 3, 2026
c6711e1
Removing support for httpAgent SDK config and providing customFetch f…
cschetan77 May 4, 2026
621dcc9
remove on-headers stale dep
cschetan77 May 4, 2026
40cae2f
Hoist RemoteJWKSet instance creation to module scope and cache it on …
cschetan77 May 4, 2026
331a8e3
enable client respects clientAssertionSigningAlg configuration test s…
cschetan77 May 4, 2026
dea71ff
Update client respects httpTimeout configuration test suite
cschetan77 May 4, 2026
6c7a7a5
Sync package lock
cschetan77 May 4, 2026
20a2a01
Account for mounted app scenarios
cschetan77 May 5, 2026
5221f75
clientAssertionSigningAlg now required for any app using PEM, Buffer,…
cschetan77 May 5, 2026
6d16d7a
Update breaking changes in V3_MIGRATION_GUIDE
cschetan77 May 5, 2026
628954a
update node ver in README and put a note for allow insecure requests
cschetan77 May 6, 2026
be90b7d
update clientAssertionSigningKey breaking change section in migration…
cschetan77 May 7, 2026
0236ae7
fix: capture sorted response types before issuer compatibility check
cschetan77 May 12, 2026
63855bc
fix: clientAssertionSigningAlg in type def file to match with joi schema
cschetan77 May 12, 2026
12b3350
fix: remove redundant normalization from TokenSet class
cschetan77 May 12, 2026
6159f01
fix: remove futoin-hkdf dep and it's usage, since node >=20 is guaren…
cschetan77 May 12, 2026
f1cf167
fix: fix: track discovery cache expiry per config instead of globally
cschetan77 May 12, 2026
4205d24
fix: handle IPv6 address while extracting port from reverse proxy's f…
cschetan77 May 12, 2026
a354554
gh workflow unit: add 20.x, 22.x and 24.x to test matrix
cschetan77 May 12, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
rl-scanner:
uses: ./.github/workflows/rl-secure.yml
with:
node-version: 18 ## depends if build requires node else we can remove this.
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 }}
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: 22
node-version: 24
require-build: false
secrets:
github-token: ${{ secrets.GITHUB_TOKEN }}
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
Comment thread
aks96 marked this conversation as resolved.

`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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we soften this claim or update it after the remaining compatibility questions are resolved ?

Right now we may still have user-visible behavior changes, like options.redirectUri no longer being honored and httpAgent no longer being used.

If those changes are intentional, they should be listed in this migration guide. If they are not intentional, we should fix them before saying there are zero public API breaking changes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Updated the migration guide with all the breaking changes, providing a summary at the start and then describing each breaking change in detail in their own section.


---

**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)
24 changes: 10 additions & 14 deletions end-to-end/custom-token-exchange.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { assert } = require('chai');
const nock = require('nock');
const { JWT } = require('jose');
const { SignJWT, importJWK } = require('jose');
const puppeteer = require('puppeteer');
const provider = require('./fixture/oidc-provider');
const { privateJWK } = require('./fixture/jwk');
Expand Down Expand Up @@ -67,19 +67,15 @@ describe('custom token exchange', async () => {
* live JWKS endpoint. { allowUnmocked: true } is required so that nock still
* passes through the provider's discovery and JWKS GET requests.
*/
const downstreamToken = JWT.sign(
{
aud: 'https://api.example.com/products',
scope: 'read:products',
},
privateJWK,
{
issuer: 'http://localhost:3001',
algorithm: 'RS256',
expiresIn: '1h',
header: { kid: 'key-1' },
},
);
const privateKey = await importJWK(privateJWK, 'RS256');
const downstreamToken = await new SignJWT({
aud: 'https://api.example.com/products',
scope: 'read:products',
})
.setProtectedHeader({ alg: 'RS256', kid: 'key-1' })
.setIssuer('http://localhost:3001')
.setExpirationTime('1h')
.sign(privateKey);

nock('http://localhost:3001', { allowUnmocked: true })
.post('/token')
Expand Down
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
Loading
Loading