local authentication with the payload token (for socket.io)?

default discord avatar
hendrik01
5 months ago
50

I am trying to integrate socket.io for some specific suff.. working fine yet.. but i need information about the user. socket.io credentials are activated, so i can see the payload-token in the cookie. any recommendations to use the payload-token and query my users collection?



const io = new SocketIOServer(server, { cors: { credentials: true } });

io.use((socket, next) => payload.authenticate(socket.request, {}, next));

io.on('connect', ({ request }) => {
  console.log(request.user);
});


Unfortunality the request.user is empty. request.islogin and request.isAuthenticated methods for example are available, but request.isAuthenticated() returns false. No idea at the moment, what i am missing here.



request.cookie has payload-token=....

  • default discord avatar
    notchr
    5 months ago

    @hendrik01 Did you call payload.authenticate() ?



    That should happen after payload.init



    And then your requests should include the user

  • default discord avatar
    hendrik01
    5 months ago

    @notchr here:

    io.use((socket, next) => payload.authenticate(socket.request, {}, next));
  • default discord avatar
    notchr
    5 months ago

    Hmm



    Interesting, I haven't seen it used in middleware like that



    Usually outside

  • default discord avatar
    notchr
    5 months ago

    Hmm, I'm guessing you've tried calling payload.authenticate() prior to your io setup?



    (and after payload.init)

  • default discord avatar
    hendrik01
    5 months ago

    @notchr basically i am running:

    payload.init(...).then(p => mysocketinitfunction(p)
  • default discord avatar
    notchr
    5 months ago

    Can you call payload.init, then call payload.authenticate()



    then do you socket init



    your



    Just to rule out the issue being related to order

  • default discord avatar
    hendrik01
    5 months ago

    Something like this?

  • default discord avatar
    notchr
    5 months ago

    Image dissapeared 😮

  • default discord avatar
    hendrik01
    5 months ago

    ops



    image.png
  • default discord avatar
    notchr
    5 months ago

    Are you set on using the .then callback pattern?



    (separate question)

  • default discord avatar
    hendrik01
    5 months ago

    no

  • default discord avatar
    notchr
    5 months ago

    ok one sec



    this is how i setup my server



    const app = express();
    const router = express.Router();
    
    app.use(express.json());
    
    const start = async () => {
      await payload.init({
        secret: process.env.PAYLOAD_SECRET,
        mongoURL: process.env.MONGODB_URI,
        express: app,
        onInit: () => {
          payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
        },
      });
    
      router.use(payload.authenticate);
    
      app.use("/", router);
      app.listen(3000, async () => {
        console.log(
          "Express is now listening for incoming connections on port 3000."
        );
      });
    };
    
    start();
  • default discord avatar
    hendrik01
    5 months ago
    image.png
  • default discord avatar
    notchr
    5 months ago

    (Note in my example i have a router because i have custom routes, but you can do the same on app)

  • default discord avatar
    hendrik01
    5 months ago
    image.png
  • default discord avatar
    notchr
    5 months ago

    @hendrik01 Any luck with that?

  • default discord avatar
    hendrik01
    5 months ago

    @notchr no, but wait i am cleaning up the code

  • default discord avatar
    notchr
    5 months ago

    Ok, no rush 🙂

  • default discord avatar
    hendrik01
    5 months ago
    const run = async () => {
      await payload.init({
        secret: process.env.PAYLOAD_SECRET,
        mongoURL: process.env.MONGODB_URI,
        express: app
      });
    
      app.use(payload.authenticate);
    
      const io = new SocketIOServer(server, {
        cors: {
          origin: process.env.FRONTEND_URI,
          credentials: true
        }
      });
    
      io.on('connect', async (socket) => {
        console.log('user', socket.request.user);
      });
    
      server.listen(3000);
    };
    
    run();


    but still user: undefined

  • default discord avatar
    notchr
    5 months ago

    Hmm



    And you confirmed that the user is in fact logged in, right?

  • default discord avatar
    hendrik01
    5 months ago

    yepp



    but i think i really need a socketio middleware to handle the authentication

  • default discord avatar
    notchr
    5 months ago

    I wonder if you can do like



    const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);
    
    io.use(wrap(payload.authenticate));
    
    // only allow authenticated users
    io.use((socket, next) => {
      const user = socket.request.user;
      if (user {
        next();
      } else {
        next(new Error("unauthorized"));
      }
    });


    Not sure if that's the same as your original attempt

  • default discord avatar
    hendrik01
    5 months ago

    it is 🙂

  • default discord avatar
    notchr
    5 months ago

    hmm



    Can we compare a normal request



    Versus the socket request



    It seems odd to me that the request wouldn't have the user in both



    Since it's a shared request context



    Like, add a get request to '/test'



    and confirm that request has the user on it

  • default discord avatar
    hendrik01
    5 months ago

    this works

  • default discord avatar
    notchr
    5 months ago
    router.get("/test", async (req, res) => {
      console.log(req.user)
    })


    oh that works?



    So that's super weird

  • default discord avatar
    hendrik01
    5 months ago
    const run = async () => {
      await payload.init({
        secret: process.env.PAYLOAD_SECRET,
        mongoURL: process.env.MONGODB_URI,
        express: app
      });
    
      app.use(payload.authenticate);
    
      app.get('/test', (req, res) => {
        return res.send(req.user);
      });
    
      const server = http.createServer(app);
    
      const io = new SocketIOServer(server, {
        cors: {
          origin: process.env.FRONTEND_URI,
          credentials: true
        }
      });
    
      io.use((socket, next) => payload.authenticate(socket.request, {}, next));
      io.use((socket, next) => {
        const { user } = socket.request;
        if (user) next();
        else next(new Error('unauthorized'));
      });
    
      io.on('connect', async (socket) => {
        console.log('user', socket.request.user);
      });
    
      server.listen(3000);
    };
    
    run();
  • default discord avatar
    notchr
    5 months ago

    I wonder if it's related to cookies

  • default discord avatar
    hendrik01
    5 months ago

    maybe related to cors?

  • default discord avatar
    notchr
    5 months ago

    maybe make sure the cors config is consistent on socket and app



    app.use(
      cors({
        origin: [
    
        ],
        credentials: true,
      })
    );


    (with creds)

  • default discord avatar
    hendrik01
    5 months ago

    it is

  • default discord avatar
    notchr
    5 months ago

    Hmm



    We may need to pull in another community member 😄

  • default discord avatar
    hendrik01
    5 months ago

    hehe

  • default discord avatar
    notchr
    5 months ago

    maybe @alessiogr has some ideas 😮

  • default discord avatar
    hendrik01
    5 months ago

    socket.request.headers.cookie has the cookie with the token

  • default discord avatar
    notchr
    5 months ago

    Oh heck yea!



    I can show you how to validate that



    If that's sufficient

  • default discord avatar
    hendrik01
    5 months ago

    it has, was not a question 🙂

  • default discord avatar
    notchr
    5 months ago

    Oh you mean you're able to verify the token?

  • default discord avatar
    hendrik01
    5 months ago

    just wanted to say that the token is listed in the socket.request.headers.cookie (because i thought maybe it is missing there and would cause maybe a problem:

    https://github.com/payloadcms/payload/blob/master/src/utilities/parseCookies.ts#L5

    )

  • default discord avatar
    notchr
    5 months ago

    So wait, is it solved? I'm confused



    You can get the user from the token



    Which is similar to getting the user on req.user

  • default discord avatar
    hendrik01
    5 months ago

    @notchr no is not solved. i just see the token, but i am not able to get the user

  • default discord avatar
    notchr
    5 months ago

    OK so you can parse the token

  • default discord avatar
    hendrik01
    5 months ago

    how can i get the user with the help of the token?

  • default discord avatar
    notchr
    5 months ago

    And get the user



    Right, so...



    // Token verification
    const verifyToken = (
      req: express.Request,
      res: express.Response
    ): string | false => {
      if (req.cookies["payload-token"]) {
        const hash = crypto
          .createHash("sha256")
          .update(process.env.PAYLOAD_SECRET)
          .digest("hex")
          .slice(0, 32);
    
        try {
          const decoded = jwt.verify(req.cookies["payload-token"], hash, {
            algorithms: ["HS256"],
          });
    
          if (!decoded) return false;
    
          console.log(decoded) // decoded user object
    
          return decoded
        } catch (err) {
          console.log("Invalid token request.", err);
          return false;
        }
      } else {
        console.log("Missing token in request.");
        return false;
      }
    };


    This is how I decode the user token on an external API



    Ideally you'd want to just get the user on the request. But until we figure that out, this should also work



    The JWT token stores the user information, and whatever additional fields you've specified to be included in the JWT

  • discord user avatar
    alessiogr
    Payload Team
    5 months ago

    oh, haven't touched any authentification stuff ever, so far. I currently only need/use api-keys 😅

  • default discord avatar
    notchr
    5 months ago

    Ahhh okay 😄



    Hmm, maybe @jacobsfletch has some ideas then? 😅

  • default discord avatar
    hendrik01
    5 months ago

    one moment



    @notchr bingo!



    // Cors
    app.use(cors({
      origin: [process.env.FRONTEND_URI || 'http://localhost:5173'],
      credentials: true,
      methods: ['GET', 'PATCH', 'POST']
    }));
    
    interface decodedUserToken extends jwt.JwtPayload {
      id: string;
      email: string;
    }
    const verifyToken = (token: string): decodedUserToken => {
      const hash = crypto
        .createHash("sha256")
        .update(process.env.PAYLOAD_SECRET)
        .digest("hex")
        .slice(0, 32);
    
      return jwt.verify(token, hash, {
        algorithms: ["HS256"],
      }) as decodedUserToken;
    };
    
    // Payload
    const run = async () => {
      await payload.init({
        secret: process.env.PAYLOAD_SECRET,
        mongoURL: process.env.MONGODB_URI,
        express: app
      });
    
      const server = http.createServer(app);
    
      const io = new SocketIOServer(server, {
        cors: {
          origin: process.env.FRONTEND_URI || 'http://localhost:5173',
          credentials: true,
          methods: ['GET', 'PATCH', 'POST']
        }
      });
    
      io.use((socket, next) => {
        const cookies = parseCookies(socket.request);
        const { id } = verifyToken(cookies['payload-token']);
    
        if (!id) next(new Error('Unauthorized'));
        socket.request.socketUserId = id;
        next();
      });
    
      io.on('connect', async (socket) => {
        console.log('user id', socket.request?.socketUserId);
      });
    
      server.listen(3000);
    };
    
    run();


    @notchr 🙂

  • default discord avatar
    notchr
    5 months ago

    YOOO



    Nicely done!

  • default discord avatar
    swnzl
    4 months ago

    Sorry, I'm a bit late to the party, but I've had the same issues. After a lot of debugging I figured out, that the request object coming from socket.io (or rather engine.io) is missing two fields that are used by payload.authenticate. Namely

    get

    and

    payload

    . So this is my solution now:


    async function run(): void {
      await payload.init({
        secret: process.env.PAYLOAD_SECRET,
        mongoURL: process.env.MONGODB_URI,
        express: app
      });
    
      const server = http.createServer(app);
      const socketServer = new SocketIOServer(server, {
        cors: {
          origin: [],
          credentials: true,
        }
      });
      socketServer.engine.use(prepareRequestForPayloadAuth);
      socketServer.engine.use(payload.authenticate);
      socketServer.use(ensureSocketAuthenticated);
    }
    function prepareRequestForPayloadAuth(req: Request, res: Response, next: NextFunction): void {
      // We need this stuff for payload JWT authentication to work properly.
      // Feel free to refactor this if any of the below referenced code doesn't
      // need this anymore or if you find a better way to add these to the request.
    
      // Required for: https://github.com/payloadcms/payload/blob/90720964953d392d85982052b3a4843a5450681e/src/auth/getExtractJWT.ts#L7
      // @ts-ignore TBH I have no idea what typescript is complaining about here...
      req.get = req.get || ((key: string) => req.headers[key.toLowerCase()]);
    
      // Required for: https://github.com/payloadcms/payload/blob/90720964953d392d85982052b3a4843a5450681e/src/auth/strategies/jwt.ts#L27
      req.payload = req.payload || payload;
    
      next();
    }
    function ensureSocketAuthenticated(socket: ClientSocket, next: (err?: Error) => void): void {
      const req = socket.request as Request;
      if (req.isUnauthenticated()) {
        next(new Error("not authorized"));
      } else {
        next();
      }
    }
    run();


    To be fair I've only tested it with an "Autorization" header, don't know if it also properly works with cookies.


    One more noteworthy thing is, that this will only validate the JWT once upon connection. If the token expires and the client doesn't reconnect, then the server won't notice.

Open the post
Continue the discussion in Discord
Like what we're doing?
Star us on GitHub!

Star

Connect with the Payload Community on Discord

Discord

online

Can't find what you're looking for?

Get help straight from the Payload team with an Enterprise License.