2

I'm using lua-resty-openidc to implement a web UI that sits in front of my backend system.

The backend provides a REST API, protected by an Authorization header containing a JWT. The frontend manages a session, and sends web users to an Identity Provider if they need to log in. When a web user has a session, the frontend must lookup the JWT, add it to an Authorization header, and proxy the request to the backend – pretty standard stuff.

Unfortunately, my backend does not cleanly distinguish between public and private resources. For example, it might have resources with the URLs:

  • /api/public/0
  • /api/public/1
  • /api/private/2
  • /api/private/3

It allows /api/public/{0,1} to be requested without an Authorization header, but /api/private/{2,3} require Authorization. The frontend has to deal with this somehow. (Note: the URLs above are simplified, the real ones don't follow a pattern and can't easily be enumerated.)

The core problem is that the frontend can't tell from the request URI whether it should trigger a login. It has to be reactive, proxying the request to the backend and checking the response code. A 401 code should force the client to login, but any other response should be returned as-is.

Because of that, I can't put my auth logic in an access_by_lua block, since they run in the access phase before the request has been sent to the upstream (backend).

I've tried moving my logic into the content phase using a body_filter_by_lua block:

location /api {
  set $session_storage shm;
  proxy_set_header X-Forwarded-Host  $http_host;
  proxy_pass https://backend;

  body_filter_by_lua_block {
    if ngx.status == ngx.HTTP_UNAUTHORIZED then
      ngx.log(ngx.INFO, 'Upstream returned a 401! Triggering auth flow')

      local opts = {
        discovery = 'https://login-server/.well-known/openid-configuration',
        scope = 'openid',
      }

      local res, err = openidc.authenticate(opts)
      if err or not res then
        ngx.status = ngx.HTTP_UNAUTHORIZED
        ngx.header.content_type = 'text/html';
        ngx.log(ngx.ERR, err)
        ngx.say("Forbidden")
        ngx.exit(ngx.HTTP_UNAUTHORIZED)
      end

    end
  }
}

… But that fails with errors (shown below). It seems like I'm too late in the request processing lifecycle to set headers and cookies:

*194 [lua] body_filter_by_lua:5: Upstream returned a 401! Triggering auth flow while sending to client, client: 10.255.1.2, server: , request: "GET /api/private/2 HTTP/1.1", upstream: "https://backend/api/private/2", host: "frontend.example.org"

*194 [lua] openidc.lua:1363: authenticate(): Error starting session: Attempt to set session cookie after sending out response headers. while sending to client, client: 10.255.1.2, server: , request: "GET /api/private/2 HTTP/1.1", upstream: "https://backend/api/private/2", host: "frontend.example.org"

*194 attempt to set ngx.status after sending out response headers while sending to client, client: 10.255.1.2, server: , request: "GET /api/private/2 HTTP/1.1", upstream: "https://backend/api/private/2", host: "frontend.example.org"

Is it possible to perform openidc.authenticate() in the content phase of Nginx request handling?

Is there a better approach I should use?

mamacdon
  • 2,899
  • 2
  • 16
  • 16

1 Answers1

1

I ended up tweaking the URL patterns used in the backend, such that private versus public URLs can be distinguished in advance. The frontend logic then becomes simple:

  • request for public resource: proxy to the backend
  • request for private resource, no Authorization header: return 401
  • request for private resource, with Authorization header: proxy to the backend

I don't know if my original question (proxying first, then making a decision based on the response code) is possible in OpenResty. But ultimately I think the new approach is cleaner anyway.

mamacdon
  • 2,899
  • 2
  • 16
  • 16