ananay / passport-apple

Passport strategy for Sign in with Apple
https://passport-apple.ananay.dev
142 stars 49 forks source link

Losing Cookies/Session after redirect #51

Open salman486 opened 5 months ago

salman486 commented 5 months ago

When apple redirects back with post request, I lose the current user session due sameSite Lax because it does not include cookie in callback with cross site post request.

unitydevadtest commented 5 months ago

The same. How to keep session like with other passports (google, facebook) with keepsessioninfo?

lsjurczak commented 5 months ago

@ananay is that doable to fix it?

salman486 commented 5 months ago

the issue will not fix by keepsessioninfo

I did below work around to fix this.

Save the session in _token cookie temporarily. It will not have sameSite property so that you can access it after redirect from apple. Notice I have added a path to this cookie so that it only get send to this path and also maxAge is very short.

      res.cookie('_token', req.session.id, {
        maxAge: 2 * 60 * 1000, // 2 minutes
        httpOnly: true,
        domain: process.env.COOKIE_DOMAIN || '/',
        path: '/v2/auth/apple/callback',
      })

When Apple call us back you can access this _token cookie and load session back. You can use below loadSession middleware in callback route.

function loadAndCreateSession(req: Request, sessionId: string) {
  return new Promise((resolve, reject) => {
    req.sessionStore.load(sessionId, function (err, session) {
      if (err) {
        reject(err)
      } else {
        if (!session?.passport.user) {
          return reject('Failed to get user from session')
        }
        // destroy old session
        req.sessionStore.destroy(sessionId)

        req.logIn(session?.passport.user, () => {
          if (session?.returnTo) {
            req.session.returnTo = session.returnTo
          }
          resolve(session)
        })
      }
    })
  })
}

const loadSession = async (req: Request, res: Response, next: NextFunction) => {
  try {
    const sessionId = req.cookies._token

    if (sessionId) {
      await loadAndCreateSession(req, sessionId)
      res.cookie('_token', '', { maxAge: 0 })
    }
  } finally {
    next()
  }
}
mifi commented 1 month ago

I found a solution to this. As mentioned by others, Apple wants to "think different" and use POST in their callback, however browser's SameSite policy doesn't allow cookies to be included in (POST) requests originating from a different site (in this case Apple). So the session does not exist and the successfuly authentication result (req.login) doesn't get saved anywhere.

A solution is to first redirect the POST request to a GET request, because a GET request can access cookies. And we include all the request body parameters as query params to the GET request:

// initial request, redirects to Apple website
router.get('/apple', passport.authenticate('apple'));

// this is the callback from Apple. Now redirect to GET with query params:
router.post('/apple/callback', express.urlencoded({ extended: true }), (req, res) => {
  const { body } = req;
  const sp = new URLSearchParams();
  Object.entries(body).forEach(([key, value]) => sp.set(key, String(value)));
  res.redirect(`/apple/callback?${sp.toString()}`);
});

// Here we handle the GET request after the redirect from the POST callback above
router.get('/apple/callback', passport.authenticate('apple', {
  successReturnToOrRedirect: '/success',
  failureRedirect: '/failure',
}));

This works even with redirectmeto.com on localhost (without https) now!