Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keystone-next Admin UI refresh loop on nginx reverse proxy #4902

Closed
MurzNN opened this issue Feb 20, 2021 · 9 comments · Fixed by #4928
Closed

Keystone-next Admin UI refresh loop on nginx reverse proxy #4902

MurzNN opened this issue Feb 20, 2021 · 9 comments · Fixed by #4928

Comments

@MurzNN
Copy link
Contributor

MurzNN commented Feb 20, 2021

Bug report

When I start Keystone-next example projects, for example "examples-next/todo", all works well when I open the Admin UI in browser directly (using listening port, http://example.com:3000/).
But when I configure reverse proxy via nginx (https://example.com/) - I got the infinite loop of page refresh:

Something went wrong.
Unhandled Runtime Error
Error: An error occurred when loading Admin Metadata
Call Stack
useKeystone
webpack-internal:///../../../../packages-next/admin-ui/context/dist/admin-ui.esm.js (415:11)

And in output of keystone-next console - repeating error:

{"level":50,"time":1613990145346,"pid":23190,"hostname":"example.com","name":"graphql","message":"Access denied","locations":[{"line":3,"column":5}],"path":["keystone","adminMeta"],"uid":"cklgg26gj0000w6xxxug1ajz9","name":"GraphQLError","stack":"node_modules/@keystone-next/admin-ui/system/dist/admin-ui.cjs.dev.js:548:19\nrunMicrotasks (<anonymous>)\n"}

To Reproduce

  1. Start the "examples-next/todo" app.
  2. Open the Admin UI interface (http://localhost:3000/) directly in browser - all works well, you will see user register form.
  3. Configure nginx host with https or even http, and reverse proxy to the app port:
location / {
  proxy_pass http://localhost:3000/;
  # Set additional headers on the upstream request
  proxy_set_header   X-Real-IP           $remote_addr;
  proxy_set_header   X-Forwarded-For     $proxy_add_x_forwarded_for;
  proxy_set_header   X-Forwarded-Proto   $scheme;
  proxy_set_header   X-Forwarded-Host    $host;
  proxy_set_header   X-Forwarded-Port    $server_port;
}
  1. Try to open the proxied domain (https://example.com/) and got the infinite loop.
  2. Same problem is with development mode (yarn dev).

Full error output:

Unhandled Runtime Error

Error: An error occurred when loading Admin Metadata
Call Stack
useKeystone
webpack-internal:///../../../../packages-next/admin-ui/context/dist/admin-ui.esm.js (415:11)
InitPage
webpack-internal:///../../../../packages-next/auth/pages/InitPage/dist/auth.esm.js (273:90)
renderWithHooks
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (14985:27)
mountIndeterminateComponent
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (17811:13)
beginWork
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (19049:16)
callCallback
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (3945:14)
invokeGuardedCallbackDev
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (3994:16)
invokeGuardedCallback
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (4056:31)
beginWork$1
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (23959:28)
performUnitOfWork
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (22771:12)
workLoopSync
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (22702:22)
renderRootSync
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (22665:7)
performSyncWorkOnRoot
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (22288:18)
flushSyncCallbackQueueImpl/<
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (11327:26)
unstable_runWithPriority
webpack-internal:///../../../../node_modules/react-dom/node_modules/scheduler/cjs/scheduler.development.js (646:12)
runWithPriority$1
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (11276:10)
flushSyncCallbackQueueImpl
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (11322:26)
flushSyncCallbackQueue
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (11309:3)
scheduleUpdateOnFiber
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (21888:9)
dispatchAction
webpack-internal:///../../../../node_modules/react-dom/cjs/react-dom.development.js (16139:26)
@MurzNN
Copy link
Contributor Author

MurzNN commented Feb 20, 2021

Here #1887 is similar problem for Keystone-v5, that can be solved (for me too) via adding:

  configureExpress: app => {
    app.set('trust proxy', true);
  },

But I can't find the right place in Keystone-next for do the same thing.
I tried to add this to node_modules/@keystone-next/keystone/src/lib/createExpressServer.ts file via:

   const server = express();
+  server.set('trust proxy', true);

but this isn't help.

@gautamsi
Copy link
Member

gautamsi commented Feb 20, 2021

new interface is not letting configuration for the express server

new interface does not have config option to configure express server, I have added a PR to enable config.server.configureServer option for this.

@MurzNN
Copy link
Contributor Author

MurzNN commented Feb 22, 2021

Seems the problem is not with trust proxy (or not only with), because even forcing set of app.set('trust proxy', true); into node_modules/express/lib/express.js file directly between or after app initialization isn't help:

function createApplication() {
// ...
  app.set('trust proxy', true);
  app.init();
  app.set('trust proxy', true);

  return app;
}

Maybe anybody knows, what else can broke the Admin UI rendering process, using reverse proxy?

If I start keystone-v5 instead of keystone-next on same place - it works, but keystone-next - not.

@MurzNN
Copy link
Contributor Author

MurzNN commented Feb 22, 2021

I have found that the first GraphQL query:

# Write your query or mutation here
{
  keystone {
    adminMeta {
      lists {
        key
        isHidden
        fields {
          path
          createView {
            fieldMode
            __typename
          }
          __typename
        }
        __typename
      }
      __typename
    }
    __typename
  }
  authenticatedItem {
    ... on User {
      id
      name
      __typename
    }
    __typename
  }
}

fails with error:

{
  "errors": [
    {
      "message": "Access denied",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "keystone",
        "adminMeta"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "Error: Access denied",
            "    at /srv/k.dev.brick.ru/domains/api.k.dev.brick.ru/packages/keystone/packages-next/admin-ui/system/dist/admin-ui.cjs.dev.js:534:19",
            "    at runMicrotasks (<anonymous>)",
            "    at processTicksAndRejections (internal/process/task_queues.js:93:5)"
          ]
        }
      },
      "uid": "cklghegau000q8hxx61gg5jca",
      "name": "GraphQLError"
    }
  ],
  "data": null
}

This line in code:

        resolve(rootVal, args, context) {
          if ('isAdminUIBuildProcess' in context || isAccessAllowed === undefined) {
            return adminMetaRoot;
          }

          return Promise.resolve(isAccessAllowed(context)).then(isAllowed => {
            if (isAllowed) {
              return adminMetaRoot;
            } // TODO: ughhhhhh, we really need to talk about errors.
            // mostly unrelated to above: error or return null here(+ make field nullable)?s


            throw new Error('Access denied');
          });
        }

@MurzNN
Copy link
Contributor Author

MurzNN commented Feb 22, 2021

I have found the source of problem! It is in isAccessAllowed(context) function, that try to compare the host of request url and host url of initial graphql query:

        isAccessAllowed: async context => {
          var _context$req, _keystoneConfig$ui2;

          // Allow access to the adminMeta data from the /init path to correctly render that page
          // even if the user isn't logged in (which should always be the case if they're seeing /init)
          const headers = (_context$req = context.req) === null || _context$req === void 0 ? void 0 : _context$req.headers;
          const url = headers !== null && headers !== void 0 && headers.referer ? new URL(headers.referer) : undefined;
          const accessingInitPage = (url === null || url === void 0 ? void 0 : url.pathname) === '/init' && (url === null || url === void 0 ? void 0 : url.host) === (headers === null || headers === void 0 ? void 0 : headers.host) && (await context.sudo().lists[listKey].count({})) === 0;
          return accessingInitPage || ((_keystoneConfig$ui2 = keystoneConfig.ui) !== null && _keystoneConfig$ui2 !== void 0 && _keystoneConfig$ui2.isAccessAllowed ? keystoneConfig.ui.isAccessAllowed(context) : context.session !== undefined);
        }

Here is failing part:

(url === null || url === void 0 ? void 0 : url.host) === (headers === null || headers === void 0 ? void 0 : headers.host)

If easier, it is do the comparison of hostname from http headers host and referrer, and fail if they isn't match.

And when I do the query directly without nginx, the values are:

host:'myproject.com:3000'
referer:'http://myproject.com:3000/init'

But via nginx proxy:

host:'localhost:3000'
referer:'http://myproject.com/init'

So my quick hacky solution is to force set right 'host' header via line like this:

proxy_set_header   Host    $host;

Is this right way, or we must tune up this comparison rule in Keystone-next to not require such unusual config of reverse proxy? Keystone-v5 works well without this "hack".

@MurzNN
Copy link
Contributor Author

MurzNN commented Feb 23, 2021

I created the PR #4928 that should fix this problem.

@maxou00
Copy link

maxou00 commented Oct 17, 2021

in case someone have this issue again, in Keystone 6, here is how i solved mine.

in keystone.ts, i temporary replaced isAccessAllowed: (context) => !!context.session?.data, with isAccessAllowed: (context) => true to disable session lock, and tried to reload the app. i worked fine, i was able to create the first admin user and when it was ok and i was able to see the admin ui, i enabled session check again.

@sladg
Copy link

sladg commented Oct 24, 2021

Hey there! Looking into same problem with Keystone6, do we have some working example how to make it work with Nginx?

@guopengliang
Copy link

I was having this similar issue in my local dev environment and found one three working solutions to solve it for my case.

Solutions:

  1. either add { secure: false } to your keystone session config,
  2. or set NODE_ENV=development when running keystone,
  3. or use https for dev environment.

For my case, it was the secure cookie not being set that caused the infinite refresh loop (when the db is not initialized) or not being able to login (when the db already has the initial admin user). When it happens, the browser console logs the error message "Error: An error occurred when loading Admin Metadata."

Looking at the login graphql response headers will find this line:

Set-Cookie: keystonejs-session=...; Max-Age=...; Path=/; Expires=...; HttpOnly; Secure; SameSite=Lax

The "Secure;" statement above declares that the cookie can only be accepted if it was via https request. For production environment, it works fine. But for development environment, we need to use the above mentioned solutions to opt-out of using secure session.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants