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

Infinite authentication loop on protected route

Open HenriqueMentormate opened this issue 1 year ago • 9 comments

Prerequisites

  • [X] I have written a descriptive issue title
  • [X] I have searched existing issues to ensure the bug has not already been reported

Fastify version

4.27.0

Plugin version

2.4.0

Node.js version

20.11.1

Operating system

macOS

Operating system version (i.e. 20.04, 11.3, 10)

14.4.1

Description

Hello. I am trying to use passport-microsoft and my issue is that after I have been authenticated and the callback URL has been reached and I am redirected to the protected route '/', I get prompted to log in again, this happens over and over again.

Here's an MRE:

"use strict";

const { readFileSync } = require("fs");
const { join } = require("path");

const fastify = require("fastify")({ logger: true });

const passport = require("@fastify/passport");
const secureSession = require("@fastify/secure-session");
const { Strategy: MicrosoftStrategy } = require("passport-microsoft");

const PORT = 3000;
const BASE_URL = "http://localhost";
const CLIENT_ID = "...";
const CLIENT_SECRET = "...";
const CALLBACK = `${BASE_URL}:${PORT}/microsoft/callback`;

// --- plugins (from the Fastify ecosystem) ---
fastify.register(secureSession, {
  key: readFileSync(join(__dirname, "secret-key")),
});
fastify.register(passport.initialize());
fastify.register(passport.secureSession());

passport.use(
  "microsoft",
  new MicrosoftStrategy(
    {
      clientID: CLIENT_ID,
      clientSecret: CLIENT_SECRET,
      callbackURL: CALLBACK,
      scope: ["user.read"],
    },
    function (accessToken, refreshToken, profile, done) {
      done(null, profile);
    }
  )
);
passport.registerUserSerializer(async (user) => user.id);
passport.registerUserDeserializer(async (user) => user);

// --- routes                               ---
const defRoutes = [
  {
    method: "GET",
    url: `/auth/login`,
    preValidation: passport.authenticate("microsoft", {
      authInfo: false,
    }),
    handler: (req, res, err, user, status) => {},
  },
  {
    method: "GET",
    url: `/microsoft/callback`,
    preValidation: passport.authenticate("microsoft", {
      authInfo: false,
      successRedirect: "/",
    }),
    handler: (req, res) => {},
  },
  {
    method: "GET",
    url: `/`,
    preValidation: passport.authenticate("microsoft", { authInfo: false }),
    handler: (req, res) => {
      return res.send(req.user);
    },
  },
];

// Add all routes into Fastify route system
for (const route of defRoutes) {
  fastify.route(route);
}

async function start() {
  try {
    fastify.listen({ port: PORT });
  } catch (e) {
    throw e;
  }
}

start();

I am not sure if I am missing anything or what the issue is.

Link to code that reproduces the bug

No response

Expected Behavior

I am not prompted to log in again when redirected to the '/' route

HenriqueMentormate avatar May 12 '24 18:05 HenriqueMentormate

Thanks, I've never used passport-microsoft, so I can't really help here.

mcollina avatar May 13 '24 10:05 mcollina

Something similar happens with passport-google-oauth20, so it does not seem to be a passport-microsoft only

MRE:

"use strict";

const { readFileSync } = require("fs");
const { join } = require("path");

const fastify = require("fastify")({ logger: true });

const passport = require("@fastify/passport");
const secureSession = require("@fastify/secure-session");
const { Strategy: GoogleStrategy } = require("passport-google-oauth20");

const PORT = 3000;
const BASE_URL = "http://localhost";
const CLIENT_ID = "...";
const CLIENT_SECRET = "...";
const CALLBACK = `${BASE_URL}:${PORT}/google/callback`;

// --- plugins (from the Fastify ecosystem) ---
fastify.register(secureSession, {
  key: readFileSync(join(__dirname, "secret-key")),
});
fastify.register(passport.initialize());
fastify.register(passport.secureSession());

passport.use(
  "google",
  new GoogleStrategy(
    {
      clientID: CLIENT_ID,
      clientSecret: CLIENT_SECRET,
      callbackURL: CALLBACK,
      scope: ['email', 'profile'],
    },
    function (accessToken, refreshToken, profile, done) {
      done(null, profile);
    }
  )
);
passport.registerUserSerializer(async (user) => user.id);
passport.registerUserDeserializer(async (user) => user);

// --- routes                               ---
const defRoutes = [
  {
    method: "GET",
    url: `/auth/login`,
    preValidation: passport.authenticate("google", {
      authInfo: false,
    }),
    handler: (req, res, err, user, status) => {},
  },
  {
    method: "GET",
    url: `/google/callback`,
    preValidation: passport.authenticate("google", {
      authInfo: false,
      successRedirect: "/",
    }),
    handler: (req, res) => {},
  },
  {
    method: "GET",
    url: `/`,
    preValidation: passport.authenticate("google", { authInfo: false }),
    handler: (req, res) => {
      return res.send(req.user);
    },
  },
];

// Add all routes into Fastify route system
for (const route of defRoutes) {
  fastify.route(route);
}

async function start() {
  try {
    fastify.listen({ port: PORT });
  } catch (e) {
    throw e;
  }
}

start();

HenriqueMentormate avatar May 13 '24 16:05 HenriqueMentormate

I'm having this same issue with passport-discord-auth as well. If I decorate any route with { preValidation: passport.authenticate('discord') }, it just tosses me back to Discord to authenticate again even though I was just there. In my callback route, I can redirect to some route, and Fastify tells me the browser asks for that route, but then Passport just sends the browser back into authentication again.

linuswillner avatar May 21 '24 22:05 linuswillner

passport-oauth2 have the behavior 🫤

Jerome1337 avatar Dec 17 '24 10:12 Jerome1337

Is this not an issue worth fixing/clarifying? Also, looked for authInfo and it does not seem to be documented...

The route in the Google OAuth video supplied in this repository just seems to request the user object without a guard, not seeing a good example of using protected routes in the documentation that don't resolve in this infinite redirect problem.

Are we doing something silly? If so, can we document that here to ensure others with the same problem can fix it?

Thanks!

andrewshawcare avatar Mar 10 '25 15:03 andrewshawcare

I have this same issue with passport-openidconnect.

I ended up using something like this for route protection in the preValidation hook

server.get(
  "/",
  {
    preValidation: (request, reply, done) => {
      if (!request.isAuthenticated()) {
        return reply.redirect("/login");
      }
      done();
    },
  },
  async (request, reply) => {
    return {
      message: `Hello!`,
      user: request.user,
      authenticated: request.isAuthenticated(),
      session: request.session,
    };
  }
);

dbrucknr avatar Apr 06 '25 14:04 dbrucknr