node.jsauthenticationsessiongraphqlkeystonejs

Keystonejs 6 Authenticate from external frontend server with AuthenticateUserWithPassword mutation


I'm building a decoupled keystonejs (keystonejs 6) app with different frontends that will pull and push data from the main keystonejs instance using its graphql api.

I've set the access control to meet my needs and everything is working fine through the AdminUI, but when I try to perform a login from one of the external frontends, no session is being created at the keystone instance.

I'm launching the mutation AuthenticateUserWithPassword and it returns the session token (so the login itself is working) and sets the keystonejs-session cookie in the browser. I also tried manually setting this cookie in the fetch() request header and performing a query, but my external frontends are not allowed to do so.

mutation AuthenticateUserWithPassword($email: String!, $password: String!) {
  authenticateUserWithPassword(email: $email, password: $password) {
    ... on UserAuthenticationWithPasswordSuccess {
      sessionToken
      item {
        email
      }
    }
    ... on UserAuthenticationWithPasswordFailure {
      message
    }
  }
}

What's interesting is that if I login from my frontend and go back to the AdminUI I'm logged in and can perform authenticated queries, and if I check the context.session when I do anything on the AdminUI I have a session object, but when I do anything from the external frontend the session is empty (but I'm using the same cookie)


Session log when the request is made from the AdminUI e.g.

session:  {
  listKey: 'User',
  itemId: '1234567890a123445656',
  data: {
    id: '1234567890a123445656',
    name: 'myUserName',
    createdAt: 'whateverdate'
  }
}

Session log when the request is made from the external frontend e.g.

session:  undefined

Apart from the keystonejs-session cookie, is there any other thing I have to send with in the headers my queries from the external frontend to make keystonejs recognize my authenticated user and use that session?


Solution

  • Ok, so as always, after a few days of working on this problem, all it took was for me to write it down in StackOverflow and a new idea came to my mind.

    After trying everything in Keystonejs documentation, I just started debugging their source code and it appears there is an undocumented authentication feature with the standard header:

    authorization: Bearer + <token>
    

    As luck would have it, I had just developed a custom bearer authentication for a different mechanism but I had no idea that keystone was checking for something in that header.

    Not only looking into it, but if a bearer is present, the session cookie is ignored:

    const token = bearer || cookies[cookieName];
    

    ( from node_modules/@keystone-6/core/session/dist/keystone-6-core-session.cjs.dev.js)

     async get({
       context
     }) {
      var _context$req$headers$;
      if (!(context !== null && context !== void 0 && context.req)) return;
      const cookies = cookie__namespace.parse(context.req.headers.cookie || '');
      const bearer = (_context$req$headers$ = context.req.headers.authorization) === null || _context$req$headers$ === void 0 ? void 0 : _context$req$headers$.replace('Bearer ', '');
      const token = bearer || cookies[cookieName];
      if (!token) return;
      try {
        return await Iron__default["default"].unseal(token, secret, ironOptions);
      } catch (err) { }
    },
    

    So, the moral of the story is: never use a Bearer token if a session cookie is present, or at least do not do so if you are using Keystonejs