Param merge fails in Express v5 because req.query is now a getter
Environment
- Node:
20.19.4 - passport-apple:
2.0.2 - @types/passport-apple:
2.0.3 - express:
5.1.0 - @nestjs/passport:
11.0.5
Overview
Inside AppleStrategy.authenticate() (passport-apple 2.0.2), the code attempts to merge req.body into req.query. Starting with Express v5, req.query is implemented as a getter, so the assigned value is ignored and remains an empty object. This causes the OAuth flow to fail because required params are never forwarded.
Steps to Reproduce
-
Send a POST request for Apple login.
- Apple auth data is in
req.body.req.queryis empty object.
- Apple auth data is in
-
Call
AppleStrategy.authenticate(). -
Inside
authenticate(), runreq.query = { ...req.query, ...req.body }to merge query/body data.- In Express v5,
req.queryis a getter, so the assignment is ignored and it stays empty. - Even if
req.bodyhas data, it is effectively discarded.
- In Express v5,
-
Call
OAuth2Strategy.authenticate()with an emptyreq.query. -
Authentication fails.
Detail
- Express.js v4
req.query(empty object)
{}
- Express.js v5
req.query(empty object)
[Object: null prototype] {}
Proposed Fixes
-
Do not assign to
req.querydirectly. UseObject.defineProperty()to setqueryinstead.- We patched the library locally with this approach and confirmed it fixes the issue.
- Caveat: This change targets Express v5’s behavior; other frameworks may react differently.
-
Create a new
reqobject (or a shallow clone) and pass that toOAuth2Strategy.authenticate.- Less efficient (extra object creation), but framework-agnostic.
Patch Example
- Before (v2.0.2)
// src/strategy.js
Strategy.prototype.authenticate = function (req, options) {
// Workaround instead of reimplementing authenticate function
req.query = { ...req.query, ...req.body };
if(req.body && req.body.user){
req.appleProfile = JSON.parse(req.body.user)
}
OAuth2Strategy.prototype.authenticate.call(this, req, options);
};
- After (Proposed)
// src/strategy.js
Strategy.prototype.authenticate = function (req, options) {
// Workaround instead of reimplementing authenticate function
Object.defineProperty(req, 'query', {
value: { ...req.query, ...req.body },
writable: true,
configurable: true,
enumerable: true,
});
if(req.body && req.body.user){
req.appleProfile = JSON.parse(req.body.user)
}
OAuth2Strategy.prototype.authenticate.call(this, req, options);
};