Infinite authentication loop on protected route
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
Thanks, I've never used passport-microsoft, so I can't really help here.
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();
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.
passport-oauth2 have the behavior 🫤
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!
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,
};
}
);