Repository: jupyterhub/jupyterhub

Issue:
Authorization form must be sent from authorization page
Hi! after upgrading to master branch (around 40 days of new commits) admins are no longer able to impersonate users, we get this error right after clicking "Authorize" on the new page:

https://github.com/jupyterhub/jupyterhub/blob/5b4f0d4304b1ea57b4d398609f2777bd93131ef4/jupyterhub/apihandlers/auth.py#L255

```
403 : Forbidden
Authorization form must be sent from authorization page
```

We are still using GitHubOAuthenticator and Postgres as backend.

Also other strange thing is happening, the admin page is not properly listing all users, but this is probably related to some of our custom page.html templates:

![image](https://user-images.githubusercontent.com/111569/47953972-e1b1c080-df84-11e8-8933-77b528c07f71.png)



Discussion:
**Update** I disabled all custom templates and the listing users issue persists.

Also, when clicking "Add Users" the new user doesn't appear. It will only appear if the user himself logs into the Hub independently of the admin.

Is this an intended changed behavior?

Found the problem, is related to being using http and https in different parts of the config settings
It seems I should switch to `c.JupyterHub.internal_ssl = True` in order to keep `http_method` consistency with my external TLS termination.
Interesting. We should add it to https://github.com/jupyterhub/jupyterhub/blob/master/docs/source/getting-started/security-basics.rst#if-ssl-termination-happens-outside-of-the-hub, no?
I think this means something is wrong in the detection of what URL is being used to make the request. How is SSL termination being implemented? Since all of these requests are made by the client with HTTPS, it shouldn't be relevant whether internal SSL is used. That would only come up if the proxy-protocol is not properly forwarding that the request is made via HTTPS.

Is this perhaps on Amazon? I believe folks have seen incorrect proxy headers coming from Amazon load-balancers. I think we need to provide the ability to override public URLs in cases where the proxy-protocol doesn't properly preserve the request protocol.
We have seen this on GCP with roughly [this](https://github.com/jupyterhub/zero-to-jupyterhub-k8s/pull/1136)
thanks for the pointer. It would be helpful if someone had a reproducible test case where this occurs (I think it's somewhere in the proxy protocol reporting http when the client request is actually https). Dumping all of the headers for the rejected request would be useful, I think.
So when I try to impersonate user:
![image](https://user-images.githubusercontent.com/2741256/57461746-dd3dcb80-7277-11e9-95cf-f4ba2365f5be.png)
I click on authorize and the following HAR (annonymized) request/response 403 is returned:
https://gist.github.com/hnykda/1e7bad208c53971b04077a88bf01f313
any solution for this ?
Is there some progress or solution? We use jupyterhub on GCP behind proxy which provides SSL connection so jupyterhub does not use internal SSL and it is quite complicated to use internal SSL in a such case.
This issue seems related to https://github.com/jupyterhub/jupyterhub/issues/2604

We faced the same issue today and did the following in our JupyterHub container: we have an nginx proxy (openresty version with lua support) started and there we added following lines as a workaround for now to the `location /` directive:
```
set $ref "";
access_by_lua_block {
  if (ngx.var.http_referer ~= nil and ngx.var.http_referer ~= "") then
      ngx.var.ref = string.gsub(ngx.var.http_referer, "https", "http")
  end
}

proxy_set_header Referer $ref;
```

You can see the full nginx.conf here: https://github.com/ml-tooling/ml-hub/blob/develop/docker-res/nginx.conf#L71

The solution comes from as the Hub compares the Referer with an internally-saved url [here](https://github.com/jupyterhub/jupyterhub/blob/17aee17c5f6a949038cc9bed8c64dad8b2b1831c/jupyterhub/apihandlers/auth.py#L250).
To pass this test, we modify the Referer header by replacing "https" with "http".
Is there any solution to this? We recently upgraded jupyterhub from 0.7.2 -> 1.0.0 and admins can no longer access user servers. When navigating to a user server, the Authorize Access page appears, followed by:

```403 : Forbidden
Authorization form must be sent from authorization page
```

We are running the hub behind a proxy/single user servers in docker containers. Having read around, I tried:
```
c.JupyterHub.internal_ssl = True
c.JupyterHub.allow_named_servers = True
```

Neither of which worked. Are there any other fixes/suggestions?

> We faced the same issue today and did the following in our JupyterHub container: we have an nginx proxy (openresty version with lua support) started and there we added following lines as a workaround for now to the `location /` directive:
> 
> ```
> set $ref "";
> access_by_lua_block {
>   if (ngx.var.http_referer ~= nil and ngx.var.http_referer ~= "") then
>       ngx.var.ref = string.gsub(ngx.var.http_referer, "https", "http")
>   end
> }
> 
> proxy_set_header Referer $ref;
> ```
> 
> You can see the full nginx.conf here: https://github.com/ml-tooling/ml-hub/blob/develop/docker-res/nginx.conf#L71
> 
> The solution comes from as the Hub compares the Referer with an internally-saved url [here](https://github.com/jupyterhub/jupyterhub/blob/17aee17c5f6a949038cc9bed8c64dad8b2b1831c/jupyterhub/apihandlers/auth.py#L250).
> To pass this test, we modify the Referer header by replacing "https" with "http".

I also found that the cause of this problem is http referer, but I'm using apache2 instead of nginx.
There's no lua_block in apache2, This is how I solve this problem in apache2:
Note: better solutions see comments by @Carreau, @bitnik and @ianalis
also see https://jupyterhub.readthedocs.io/en/stable/reference/config-proxy.html
```
RewriteEngine on
<Location /jupyter>
    ProxyPass http://localhost:8000/jupyter
    ProxyPassReverse http://localhost:8000/jupyter

    # important config 
    RewriteCond %{HTTP_REFERER} /jupyter/hub/api/oauth2/ [NC] 
    RewriteRule /jupyter/(.*) - [E=REFERER:%{SERVER_NAME}/jupyter/$1]

    # thanks to @Carreau, fix a mistake here
    RequestHeader set Referer http://%{REFERER}e env=REFERER

    # important config end

    RewriteCond %{HTTP:UPGRADE} WebSocket$ [NC]
    RewriteCond %{HTTP:CONNECTION} Upgrade$ [NC]
    RewriteRule /jupyter/(.*) ws://localhost:8000/jupyter/$1 [P,L]
</Location>
```
Rewrite engine can set environment virable by `[E=REFERER:%{SERVER_NAME}/jupyter/$1]`
RequestHeader is similar to "proxy_set_header" in niginx

@limimiking solution worked for me. 
Well I spoke too soon, that broke other stuff. 


```
[W 2019-08-28 11:16:29.068 JupyterHub base:60] Blocking Cross Origin API request.  Referer: 
 http://(null), Host: <mydomain>/jupyter/hub/
```
I fixed it with 

```
RequestHeader set Referer http://%{REFERER}e env=REFERER
```

So the RequestHeader is set only if REFERER has been defined by the previous rule. 
From tornado source code (https://github.com/tornadoweb/tornado/blob/v6.0.3/tornado/httputil.py#L314-L318):

> The protocol used, either "http" or "https".  If ``HTTPServer.xheaders``
       is set, will pass along the protocol used by a load balancer if
       reported via an ``X-Scheme`` header.

So I set the `X-Scheme` header in my nginx config and this fixed the problem:

```
    # Managing literal requests to the JupyterHub front end
    location / {
        ...
        proxy_set_header X-Scheme $scheme;
    }
```
@limimiking's solution with @Carreau's correction worked for me as well but @bitnik's solution is easier. For apache, I added this single line to the config:

    RequestHeader set "X-Scheme" expr=%{REQUEST_SCHEME}
> I fixed it with
> 
> ```
> RequestHeader set Referer http://%{REFERER}e env=REFERER
> ```
> 
> So the RequestHeader is set only if REFERER has been defined by the previous rule.

My fault, I also encountered this problem later.
Thank you for your correction, worked for me pretty well.
@bitnik solution is work for me on my caddy proxy
```
proxy / jupyterhub {
    header_upstream X-Scheme {scheme}
    transparent
    websocket
    timeout 0
}
```
@yuvipanda this issue was automatically closed by https://github.com/berkeley-dsep-infra/datahub/commit/ae18ef85490dde261981b08d1e8a6b25a4d22dd5
Was that unintentional?
This issue has been mentioned on **Jupyter Community Forum**. There might be relevant details there:

https://discourse.jupyter.org/t/authorization-form-must-be-sent-from-authorization-page/3081/1

@manics I think it was a mistake, but the same code is now available as part of z2jh 0.9.0-beta.3 though, but that is only related to a z2jh deployment rather than JupyterHub in general. I'm reopening this for now.
With latest JupyterHub I also had issues in accessing other users' servers while being an admin.
I use JupyterHub with SwarmSpawner and OAuth authentication.
I have an HAproxy in front of JupyterHub handling TLS termination.
I added this to HAproxy config to fix the scheme mismatch:
```
http-request add-header X-Scheme https if { ssl_fc }
```
To note that I already had the following, but it was not enough:
```
http-request add-header X-Forwarded-Proto https if { ssl_fc }
```
Now with both headers added, things do work without breaking other pieces.
Thank you all for the tips!
If you're running istio like we do, just add something like
```
headers:
  request:
    set:
      "x-scheme": "https"
```
Anyone solved that issue using the jupyterhub helm chart [jupyterhub/zero-to-jupyterhub-k8s](https://github.com/jupyterhub/zero-to-jupyterhub-k8s)?
We're also running into this issue with the "Zero to JupyterHub" Helm chart (Kubernetes). SSL is terminated at the AWS load balancer (`proxy.https.type: offload`).
I don't think any of the above workarounds apply in that scenario?
@itssimon what version of the helm chart is used?
Chart version: jupyterhub-0.9.0
App version: 1.1.0
Same issue here with:

Chart version: jupyterhub-0.9.0
App version: 1.1.0

Using LDAP auth. Worked with chart 0.8.2

Using NGINX reverse proxy for https
This issue has been mentioned on **Jupyter Community Forum**. There might be relevant details there:

https://discourse.jupyter.org/t/use-caddy-as-a-proxy/4452/1

This issue has been mentioned on **Jupyter Community Forum**. There might be relevant details there:

https://discourse.jupyter.org/t/use-caddy-as-a-proxy/4452/2

@yuvipanda [your commit](https://github.com/yuvipanda/2i2c-jupyterhub-deploys/commit/ae2f63917d574495d4e0c4849781a6543c8b59f2) autoclosed this again!
Same problem. Using Traefik as a reverse proxy. Posting just to keep thread open.
Same here with GCP Loadbalancer
Managed to get this working by adding this config to my [Gloo (Envoy wrapper)](https://gloo.solo.io/) config:
```
<snip>
    routes:
      - matchers:
          - methods: []
            prefix: /
        options:
          headerManipulation:
            requestHeadersToAdd:
              - header:
                  key: X-Scheme
                  value: https
                append: false
              - header:
                  key: X-Forwarded-Proto
                  value: https
                append: false
<snip>
```
@zifeo The issue with [jupyterhub/zero-to-jupyterhub-k8s](https://github.com/jupyterhub/zero-to-jupyterhub-k8s) for me is related to the proxy. The proxy adds its own x-forward headers to the ones created by the service offloading SSL. I offload SSL at the load balancer.
```
x-forwarded-port=443,80
x-forwarded-proto=https,http
```
Editing the proxy deployment adding `--no-x-forward` passes the headers to the hub unaltered by the proxy.
> @zifeo The issue with [jupyterhub/zero-to-jupyterhub-k8s](https://github.com/jupyterhub/zero-to-jupyterhub-k8s) for me is related to the proxy. The proxy adds its own x-forward headers to the ones created by the service offloading SSL. I offload SSL at the load balancer.
> 
> ```
> x-forwarded-port=443,80
> x-forwarded-proto=https,http
> ```
> 
> Editing the proxy deployment adding `--no-x-forward` passes the headers to the hub unaltered by the proxy.

@0nebody and this fixed it for you? (we are also offloading ssl at the NGINX reverse proxy)
@stefanvangastel Yes, it works for me as the AWS ALB adds the correct headers. Assuming your Nginx configuration sets the x-forwarded headers than it should be the same.

If the proxy doesn't have `--no-x-forward` set it appends its own `x-forwarded-proto` to the one added by the ALB for http. I assume this is why people are having success setting `x-scheme`.
Hmm. Nginx sets the correct forwarded scheme and proto (https), deployment of proxy modified to use the `--no-x-forward` flag. Still getting the "Authorization form must be sent from authorization page".
This issue has been mentioned on **Jupyter Community Forum**. There might be relevant details there:

https://discourse.jupyter.org/t/gateway-error-with-haproxy-in-front-of-kubernetes/5821/5

@bitnik's [tip](https://github.com/jupyterhub/jupyterhub/issues/2284#issuecomment-527196161) worked for me when using nginx for ssl termination.
@0nebody adding `--no-x-forward` doesn't work for OKD 4.5 (haproxy underneath), throwing too many redirects with this in unfortunately.
@elgalu did this commit fix it for you? https://github.com/elgalu/jupyterhub/commit/5e687c095542be6799d282ed23a1fa130e0c6f23 If yes, could you submit a PR with it?
yes, my commit fixed the problem for us but it's a desperate workaround, isn't it?
@elgalu I think we're all desperate here... seems to be an issue plaguing us since 2018. In OKD 3.11 I've fixed it by patching the openshift router template as per https://github.com/jupyterhub/jupyterhub/issues/2284#issuecomment-578118761 but that's an even worse approach. I see quite some folks implementing workarounds for this left and right when hopefully it can be fixed here.

Resolving patch:
diff --git a/jupyterhub/apihandlers/auth.py b/jupyterhub/apihandlers/auth.py
--- a/jupyterhub/apihandlers/auth.py
+++ b/jupyterhub/apihandlers/auth.py
@@ -274,9 +274,26 @@ def post(self):
         uri, http_method, body, headers = self.extract_oauth_params()
         referer = self.request.headers.get('Referer', 'no referer')
         full_url = self.request.full_url()
-        if referer != full_url:
+        # trim protocol, which cannot be trusted with multiple layers of proxies anyway
+        # Referer is set by browser, but full_url can be modified by proxy layers to appear as http
+        # when it is actually https
+        referer_proto, _, stripped_referer = referer.partition("://")
+        referer_proto = referer_proto.lower()
+        req_proto, _, stripped_full_url = full_url.partition("://")
+        req_proto = req_proto.lower()
+        if referer_proto != req_proto:
+            self.log.warning("Protocol mismatch: %s != %s", referer, full_url)
+            if req_proto == "https":
+                # insecure origin to secure target is not allowed
+                raise web.HTTPError(
+                    403, "Not allowing authorization form submitted from insecure page"
+                )
+        if stripped_referer != stripped_full_url:
             # OAuth post must be made to the URL it came from
-            self.log.error("OAuth POST from %s != %s", referer, full_url)
+            self.log.error("Original OAuth POST from %s != %s", referer, full_url)
+            self.log.error(
+                "Stripped OAuth POST from %s != %s", stripped_referer, stripped_full_url
+            )
             raise web.HTTPError(
                 403, "Authorization form must be sent from authorization page"
             )


Test patch: