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?