passport-apple icon indicating copy to clipboard operation
passport-apple copied to clipboard

Param merge fails in Express v5 because req.query is now a getter

Open archepro84 opened this issue 5 months ago • 0 comments

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

  1. Send a POST request for Apple login.

    • Apple auth data is in req.body. req.query is empty object.
  2. Call AppleStrategy.authenticate().

  3. Inside authenticate(), run req.query = { ...req.query, ...req.body } to merge query/body data.

    • In Express v5, req.query is a getter, so the assignment is ignored and it stays empty.
    • Even if req.body has data, it is effectively discarded.
  4. Call OAuth2Strategy.authenticate() with an empty req.query.

  5. Authentication fails.

Detail

  • Express.js v4 req.query (empty object)
{}
  • Express.js v5 req.query (empty object)
[Object: null prototype] {}

Proposed Fixes

  1. Do not assign to req.query directly. Use Object.defineProperty() to set query instead.

    • 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.
  2. Create a new req object (or a shallow clone) and pass that to OAuth2Strategy.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);
  };

References

archepro84 avatar Jul 22 '25 05:07 archepro84