I am trying to integrate Steam accounts into my project, by linking them through passport-steam
module.
My app is also using express-session
, in order to authenticate signed in users.
const session = require("express-session");
const Redis = require("ioredis");
const RedisStore = require("connect-redis")(session);
const redisClient = new Redis({
"host": "…",
"port": 6379,
"password": "…"
});
For example, upon signing in:
app.get("/sign-in/", function(req, res, next) {
req.session._id = req.session._id;
req.session.token = req.session.token;
res.sendStatus(200);
});
These accounts are intended to be linkable to Steam accounts.
const passport = require("passport");
const SteamStrategy = require("passport-steam").Strategy;
passport.use(new SteamStrategy({
"returnURL": "https://www.example.com/link/steam/redirect/",
"realm": "https://www.example.com/",
"apiKey": "…"
}, function(identifier, profile, done) {
profile.identifier = identifier;
return done(null, profile);
}));
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(user, done) {
done(null, user);
});
app.use(session({
"resave": false,
"saveUninitialized": false,
"secret": "…",
"store": new RedisStore({ "client": redisClient }),
"cookie": {
"secure": true,
"sameSite": "none"
}
}));
app.use(passport.initialize());
app.use(passport.session());
When a user signs in successfully in Steam, the Express Session disappears completely. I tried to resave the Express Session like so, but I still couldn't retain it:
app.get("/link/steam/", function(req, res, next) {
req.session._id = req.session._id;
req.session.token = req.session.token;
req.session.urlExtension = language.urlExtension;
req.session.save(function() {
passport.authenticate("steam")(req, res, next);
});
});
app.get("/link/steam/redirect/", passport.authenticate("steam", { "failureRedirect": "/" }), async function(req, res) {
console.log(req.session._id); // undefined
console.log(req.session.token); // undefined
console.log(req.user.steamId); // The Steam ID is obtained
});
This was a very stubborn problem, but I managed to find a workaround.
First, before we redirect the user to Steam, we can generate a state – a secret, unique one-time ID (random 16-byte hex string) to tie the session to the Steam flow.
const crypto = require("crypto");
app.get("/link/steam/", async function(req, res, next) {
const state = crypto.randomBytes(16).toString("hex");
await redisClient.set(`steam_state:${state}`, JSON.stringify({
"_id": req.session._id,
"token": req.session.token
}), "EX", 300); // <- If the user doesn’t complete Steam login in 5 minutes, this state is discarded.
res.cookie("steam_state", state, {
"httpOnly": true,
"secure": true,
"sameSite": "none"
});
passport.authenticate("steam")(req, res, next);
});
In the Steam callback handler, Passport validates the Steam login response as normal. Afterwards, we log the user in: req.logIn(user, async function(err) {});
Inside the function, we get the state
from the secure cookie: const state = req.cookies?.steam_state;
Finally, we restore the session: const data = await redisClient.get(`steam_state:${state}`);
Just in case, we save the updated session: await new Promise(resolve => req.session.save(resolve));
app.get("/link/steam/redirect/", function(req, res, next) {
passport.authenticate("steam", async function(err, user) {
if (err || !user) {
return res.redirect("/");
}
req.logIn(user, async function(err) {
if (err) {
return next(err);
}
const steamId = req.user.id; // Obtaining the Steam ID as expected
const state = req.cookies?.steam_state;
if (state) {
try {
const data = await redisClient.get(`steam_state:${state}`);
if (data) {
const parsedData = JSON.parse(data);
req.session._id = parsedData._id;
req.session.token = parsedData.token;
// Clearing the cookie
await redisClient.del(`steam_state:${state}`);
res.clearCookie("steam_state", {
"httpOnly": true,
"sameSite": "none",
"secure": isHTTPS
});
await new Promise(resolve => req.session.save(resolve));
}
}
catch (err) {
console.error(err);
}
}
// Outputs the Express Session
console.log(req.session._id);
console.log(req.session.token);
// Outputs the Steam ID as well
console.log(steamId);
// …
});
})(req, res, next);
});
Middlewares must include cookie-parser
as well:
app.use(session({
"resave": false,
"saveUninitialized": false,
"secret": "…",
"store": new RedisStore({ "client": redisClient }),
"cookie": {
"secure": "true",
"sameSite": "none"
}
}));
const cookieParser = require("cookie-parser");
app.use(cookieParser()); // <-
app.use(passport.initialize());
app.use(passport.session());