twz123 / oidc-reverse-proxy

Very basic OpenID Connect HTTP Reverse Proxy
MIT License
6 stars 4 forks source link

Internal Server Error when first request after session expiration is an XHR #3

Open fungusakafungus opened 6 years ago

fungusakafungus commented 6 years ago

I'm using oidc-reverse-proxy with dex and kubernetes dashboard.

When I log in into k8s dashboard (fronted by oidc-reverse-proxy) and leave my browser untouched for longer than -session-inactivity-threshold, then click on something in the dashboard, the dashboard shows nicely styled "500 Internal Server Error".

In chrome dev tools, I see:

  1. a request to https://dashboard.auth.hardway.testk8s.lan/api/v1/login/status returning 307 Temporary Redirect
  2. a request to https://dex.auth.hardway.testk8s.lan/dex/auth?client_id=loginapp&redirect_uri=https%3A%2F%2Fdashboard.auth.hardway.testk8s.lan%2F&response_type=code&scope=openid+email+profile+groups&state=... returning 302
  3. a request to https://dashboard.auth.hardway.testk8s.lan/api/v1/cluster?... returning 500 Internal Server Error

In logs of oidc-reverse-proxy I see:

I0322 09:22:48.338571       1 redirect.go:30] 127.0.0.1:32848 /api/v1/login/status <<< 307 Temporary Redirect - https://dex.auth.hardway.testk8s.lan/dex/auth?client_id=loginapp&redirect_uri=https%3A%2F%2Fdashboard.auth.hardway.testk8s.lan%2F&response_type=code&scope=openid+email+profile+groups&state=amkcKmXmBRQfwe7nEvPA7ttj
E0322 09:22:48.423429       1 errors.go:22] [::1]:40332 /api/v1/cluster?filterBy=&itemsPerPage=10&name=&namespace=&page=1&sortBy=d,creationTimestamp <<< 500 Internal Server Error - paths don't match: expected /, got /api/v1/cluster

and after browser reload:

E0322 09:22:54.395604       1 errors.go:22] 127.0.0.1:32854 / <<< 500 Internal Server Error - state didn't match: expected amkcKmXmBRQfwe7nEvPA7ttj, got 

Interestingly enough, when I let even more time pass, another reload would heal it (redirect me to dex login page and it'll work afterwards)

fungusakafungus commented 6 years ago

By the way, keycloak-proxy seems to have a similar problem: It uses identity lifetime from dex jwt token (which is 24h per default) for cookie expiry time. I reduced it in dex and could see stale dashboard contents(the dashboard content wouldn't change when clicking links) after token expiry. Showing 500 Internal Server Error is better than that :)

twz123 commented 6 years ago

Hey! Thanks for reporting! I finally got the time to investigate. And then it turned out to be a can of worms :confused: I'll try to summarize my observations here, maybe there's a way to solve this, although I'm not so sure about that.

As far as I can tell, there are two major problems:

  1. The k8s dashboard effectively does two XHR calls in parallel for each click on the UI. One to the actual resource endpoint, and one additional request to /api/v1/login/status. If the session is expired, the one request that hits the proxy first will get its redirect response. The second one will reach the proxy when it's actually awaiting the callback from the IdP, hence the proxy reacts with a quite unwelcoming 500 response. So I explored the naive path and tried to replace that 500 response with the same redirect that was used for the first request. But then I quickly realzed that the initial URL that has been requested by the client is stored in the session: When the callbacks come back from the IdP, both requests will be redirected to one single (the first) URL. To address this issue, I played around with the redirect URL that is being sent to the IdP. I actually appended the initial URL as a query parameter to it. While this is a bit ugly, it seems to be working, as long as the IdP allows for custom query parameters in the redirect URL (which is not a very widespread feature, I guess?). You may have a look at my attempt, if you like. Any ideas about that?

  2. and probably biggest show-stopper: XHR CORS requests won't include cookies, of course :sigh: Same-orign policy, you know? This means that the XHR gets properly redirected to the IdP, but without cookies. In my test setup, this lead to 200 OK responses containing the login form. But this actually doesn't trigger an error in the k8s dashboard, it just doesn't refresh its detail views. One might try to circumvent this problem with some nasty ingress tricks to get the dashboard and the IdP issuer URL into the same subdomain...

Sooo. Overall not that promising. Hope I did an acceptable job in describing the issues in a more or less understandable way. One obvious way of mitigating this problem is to use longer session / token lifetimes. And, reloading the dashboard also works (without login of course).

So if you have any input on these things, let me know...

fungusakafungus commented 6 years ago

dex IdP does some stuff with CORS here: https://github.com/coreos/dex/blob/6ef8cd512fb9e075bf7d70ec3fe74a8397eadde4/server/server.go#L261-L267 ...so I would think that CORS is explicitly not enabled for /auth/* path, because... I don't know, it would be insecure?

JS application (k8s dashboard) should probably react differently to 307 or 401 with Location:: it should execute top frame navigation instead of following that redirect per XHR. Maybe transparent authenticating proxy for single-page apps is not even possible?