admin管理员组

文章数量:1122826

What is expected?

  • I hit the GET /csrf/token endpoint from my frontend
  • A new csrf token is generated as no cookie is set yet
  • This csrf token is saved to redis store via connect-redis and connect.sid with sessionId is stored as a cookie on the browser
  • I hit POST /login endpoint with username and password that requires CSRF token to be present and this works

What is happening?

  • The csrf token is saved to redis store but I don't see a connect.sid cookie on my Firefox/Chrome/Safari browser
  • req.session.csrfToken returns undefined even though the value is literally saved inside redis

Backend

  • My express backend app.js
require("dotenv-flow").config();
const cors = require("cors");
const http = require("http");
const express = require("express");
const passport = require("passport");
const RedisStore = require("connect-redis").default;
const Redis = require("ioredis");
const expressSession = require("express-session");
const { Strategy: LocalStrategy } = require("passport-local");
const { Server } = require("ws");
const helmet = require("helmet");
const { csrfSync } = require("csrf-sync");

const { generateToken, csrfSynchronisedProtection } = csrfSync();

const client = new Redis({
  host: process.env.REDIS_SESSION_HOST,
  port: process.env.REDIS_SESSION_PORT,
  password: process.env.REDIS_SESSION_PASSWORD,
  db: process.env.REDIS_SESSION_DB,
});

const store = new RedisStore({ client });

const loggedInUser = {
  userId: 1,
  userName: process.env.TEST_USER_EMAIL,
  isAdmin: false,
};

const sessionParser = expressSession({
  secret: process.env.SESSION_SECRET,
  resave: process.env.SESSION_RESAVE === "true",
  rolling: process.env.SESSION_ROLLING === "true",
  saveUninitialized: process.env.SESSION_SAVE_UNINITIALIZED === "true",
  cookie: {
    httpOnly: process.env.SESSION_HTTP_ONLY === "true",
    // Doesnt work if maxAge is not of type Number
    maxAge: +process.env.SESSION_MAX_AGE,
    // 
    // 
    sameSite: process.env.SESSION_SAME_SITE === "true",
    secure: process.env.SESSION_SECURE === "true",
  },
  store,
});

const app = new express();

app.use(
  cors({
    origin: "http://localhost:3000",
    credentials: true,
  })
);

// 
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        frameAncestors: ["'self'", ";],
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
        styleSrc: ["'self'", "https:", "'unsafe-inline'"],
        baseUri: ["'self'"],
        fontSrc: ["'self'", "https:", "data:"],
        imgSrc: [
          "'self'",
          "data:",
          ";,
          ";,
        ],
      },
    },
    referrerPolicy: {
      policy: "same-origin",
    },
  })
);

passport.serializeUser((user, done) => {
  done(null, user.userId);
});
passport.deserializeUser(async (userId, done) => {
  done(null, loggedInUser);
});

passport.use(
  "local",
  new LocalStrategy(
    {
      usernameField: "email",
      passwordField: "password",
      badRequestMessage: "email or password is missing",
    },
    async (email, password, done) => {
      if (
        email === process.env.TEST_USER_EMAIL &&
        password === process.env.TEST_USER_PASSWORD
      ) {
        return done(null, loggedInUser);
      } else {
        return done(null, false);
      }
    }
  )
);

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(sessionParser);
app.use(passport.initialize());
app.use(passport.session());

app.get("/user", (req, res) => {
  return res.json(req.user);
});

app.get("/csrf/token", (req, res) => {
  req.session.test = 'abracadabra';
  return res.json({ token: generateToken(req) });
});

app.get("/session/token", (req, res) => {
  return res.json({ token: req.session.csrfToken, test: req.session.test });
});

app.post("/login", csrfSynchronisedProtection, (req, res, next) => {
  passport.authenticate("local", {}, async (error, user, info) => {
    if (error) {
      return next(error);
    }
    if (!user) {
      return res.json(false);
    }
    req.logIn(user, (error) => {
      if (error) {
        return next(error);
      }
      return res.json(user);
    });
  })(req, res, next);
});

app.post("/logout", (req, res, next) => {
  req.logout();
  req.session.destroy((err) => {
    if (err) {
      return next(err);
    }
    req.user = null;
    res.clearCookie("connect.sid");
    return res.json(true);
  });
});

const map = new Map();

const server = http.createServer(app);

const websocketServer = new Server({ noServer: true });

server.on("upgrade", (request, socket, head) => {
  sessionParser(request, {}, () => {
    console.log(
      request.session,
      request.user,
      request.session.user,
      request.headers.cookie
    );

    websocketServer.handleUpgrade(request, socket, head, function (ws) {
      websocketServer.emit("connection", ws, request);
    });
  });
});

websocketServer.on("connection", function (ws, request) {
  const user = request.session.user;

  map.set(user, ws);

  ws.on("message", function (message) {
    //
    // Here we can now use session parameters.
    //
    console.log(`Received message ${message} from user ${user}`);
  });

  ws.on("close", function () {
    map.delete(user);
  });
});

server.listen(+process.env.PORT, () =>
  console.log(`server listening on ${process.env.PORT}`)
);

  • My .env.development file
PORT='8000'
REDIS_SESSION_DB='5'
REDIS_SESSION_HOST='localhost'
REDIS_SESSION_PASSWORD='somepassword'
REDIS_SESSION_PORT='6379'
SESSION_HTTP_ONLY='true'
SESSION_MAX_AGE='86400000'
SESSION_NAME='ch_test'
SESSION_RESAVE='false'
SESSION_ROLLING='false'
SESSION_SAME_SITE='true'
SESSION_SAVE_UNINITIALIZED='false'
SESSION_SECRET='abracadabrafoobarbaz'
SESSION_SECURE='false'
TEST_USER_EMAIL='[email protected]'
TEST_USER_PASSWORD='123456789'

Frontend

  • It is a Nuxt 2 project and I ll include only the relevant files here
  • The store file below executes on the server side first inside nuxtServerInit and sends a GET /csrf/token store/index.js
export const state = () => ({
  csrfToken: null,
  redirect: null,
})

export const mutations = {
  SET_CSRF_TOKEN(state, csrfToken) {
    state.csrfToken = csrfToken
  },
  SET_REDIRECT(state, redirect) {
    state.redirect = redirect
  },
}

export const actions = {
  async getCsrfToken({ commit }) {
    try {
      const { data } = await this.$axios.get('/csrf/token')
      commit('SET_CSRF_TOKEN', data.token)
      console.log('SAVE CSRF TOKEN', data.token)
    } catch (error) {
      console.error(error)
    }
  },
  async nuxtServerInit({ commit, dispatch }, { $dayjs, req }) {
    await dispatch('getCsrfToken')
  },
}
  • I wrote an axios plugin that uses @nuxtjs/axios to get this csrf token from the vuex store (nuxtServerInit stores it first) and then send it as header plugins/axios.js
export default ({ $axios, store }) => {
  // Inject $hello(msg) in Vue, context and store.
  $axios.defaults.timeout = 30000

  // 
  $axios.defaults.transitional.clarifyTimeoutError = true

  $axios.onRequest((config) => {
    const csrfToken = store.state.csrfToken
    console.log(config.url, 'ON REQUEST', csrfToken)

    if (
      csrfToken &&
      ['get', 'post', 'put', 'delete', 'patch'].includes(config.method)
    ) {
      config.headers['X-CSRF-Token'] = csrfToken
    }
    return config
  })
}

Libraries

  • express-session
  • connect-redis
  • ioredis
  • cors
  • csrf-sync
  • helmet

Download link

  • DOWNLOAD THE BACKEND HERE
  • DOWNLOAD THE FRONTEND HERE

Questions

  • I am overworked and probably stressed out badly and I have somehow messed this up
  • Kindly help me understand why connect.sid cookie is not stored on the browser side

UPDATE 1

  • If I remove the csrf middleware, everything seems to work really well and connect.sid is created as expected

本文标签: