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

[CORS] No 'Access-Control-Allow-Origin' header is present on the requested resource. #303

Open
alexanderwolz opened this issue May 6, 2023 · 8 comments

Comments

@alexanderwolz
Copy link
Contributor

alexanderwolz commented May 6, 2023

Hi @Joxit,
I really want to integrate your product in our tech stack but I'm struggling with CORS now for days.
I basically used token-auth-keycloak as a template but its not working for me.

My Setup:

  • Single Host Docker Environment with Compose
    • Keycloak 21.1.1 (Docker)
    • Registry 2.8.1 (Docker)
    • Nginx 1.24.0 as Reverse Proxy (Docker)
    • Registry UI 2.4.1 (Docker) (also tested with main)
    • Registry and Registry-UI are served at the same domain (registry at /v2 and ui at /)
  • They are all on the same docker network and can access each other

I successuflly log in with the docker client using the credentials provided by my Keycloak auth server.

Bug description

When I open the front page of registry UI, I can see that two resources are loaded:

Chrome shows me an error like
Access to XMLHttpRequest at 'https://my-auth-server.com/protocol/docker-v2/auth?service=myService&scope=registry:catalog:' from origin 'https://my-registry-domain.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
docker-registry-ui.js:40 GET https://my-auth-server.com/protocol/docker-v2/auth?service=myService&scope=registry:catalog:
net::ERR_FAILED 401 (onAuthentication)

I now tried every possible combination with REGISTRY_URL at https://my-registry-domain.com or NGINX_PROXY_PASS_URL at http://registry:5000. Both settings have the same effect. I also tried a simple JavaScript snippet to load the my-auth-server.com url via XMLHttpRequest (withCredentials) and got a Basic Auth prompt and received the JSON Web Token, but it does not work with the my-registry-domain.com url.

I also adapted my Reverse Proxy to set CORS headers for Keycloak and Registry, I see the CORS headers returned on OPTION and GET using curl for both URLs ( using curl -vX GET "INSER_URL_HERE" -H 'Origin: https://my-registry-domain.com'

I also added the CORS Headers to the Registry environments but that also don't seem to work..

Do you have an Idea what I'm missing?

How to Reproduce

Setup this constellation and open the front page.

Setup Files

My docker-compose file

  registry:
    container_name: registry
    hostname: registry
    image: registry:2.8.1
    restart: unless-stopped
    environment: 
      REGISTRY_HTTP_SECRET: ${HTTP_SECRET}
      REGISTRY_STORAGE_DELETE_ENABLED: "true"
      REGISTRY_AUTH_TOKEN_REALM: ${KEYCLOAK_AUTH_URL}
      REGISTRY_AUTH_TOKEN_SERVICE: ${KEYCLOAK_CLIENT_ID}
      REGISTRY_AUTH_TOKEN_ISSUER: ${KEYCLOAK_ISSUER}
      REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /opt/certs/localhost_trust_chain.pem
      REGISTRY_HTTP_HEADERS_X-CONTENT-TYPE-OPTIONS: '[nosniff]'
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: "['${PUBLIC_URL}']"
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Credentials: "[true]"
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers: "['Authorization', 'Accept', 'Cache-Control']"
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods: "['HEAD', 'GET', 'OPTIONS', 'DELETE']"
      REGISTRY_HTTP_HEADERS_Access-Control-Max-Age: "[1728000]"
      REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers: "['Docker-Content-Digest']"
    volumes:
      - data:/var/lib/registry:rw
      - certs:/opt/certs/:ro

  registry_ui:
    container_name: registry_ui
    hostname: registry_ui
    #image: joxit/docker-registry-ui:2.4.1
    image: joxit/docker-registry-ui:main
    restart: unless-stopped
    user: nginx
    environment:
      SINGLE_REGISTRY: "true"
      DELETE_IMAGES: "true"
      SHOW_CONTENT_DIGEST: "true"
      USE_CONTROL_CACHE_HEADER: "true"
      #NGINX_PROXY_PASS_URL: http://registry:5000
      REGISTRY_URL: ${PUBLIC_URL}
    depends_on:
      - registry

   skipped other stuff

my Registry and UI proxy settings

server {
  listen                  443 ssl http2;
  listen                  [::]:443 ssl http2;
  server_name             my-registry-domain.com;
  
 [ssl stuff]

  # Docker Registry API - see https://docs.docker.com/registry/recipes/nginx/
  location /v2/ {

    # Do not allow connections from docker 1.5 and earlier
    # docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
    if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
      return 404;
    }

    add_header        Docker-Distribution-Api-Version $docker_distribution_api_version always;

    add_header        Access-Control-Allow-Origin $allow_origin always;
    add_header        Access-Control-Allow-Methods "OPTIONS, GET" always;
    add_header        Access-Control-Allow-Headers "Content-Type, Accept, Authorization, Cache-Control" always;
    add_header        Access-Control-Allow-Credentials true always;
    add_header        Access-Control-Max-Age 1728000;
    add_header        Access-Control-Expose-Headers WWW-Authenticate always;

    if ($request_method = 'OPTIONS') {
        add_header        Access-Control-Allow-Origin $allow_origin always;
        add_header        Access-Control-Allow-Methods "OPTIONS, GET" always;
        add_header        Access-Control-Allow-Headers "Content-Type, Accept, Authorization, Cache-Control" always;
        add_header        Access-Control-Allow-Credentials true always;
        add_header        Access-Control-Max-Age 1728000;
        add_header        Access-Control-Expose-Headers WWW-Authenticate always;
        add_header        Content-Type 'text/plain charset=UTF-8';
        add_header        Content-Length 0;
        return            204;
    }

    set $upstream         registry:5000;
    proxy_pass            http://$upstream;
  }

  # Docker Registry UI
  location / {
    set $upstream         registry_ui:8080;
    proxy_pass            http://$upstream;
  }

}

my Keycloak proxy settings:

  [...]

  # Docker authentication
  location /realms/{my-realm}/protocol/docker-v2/auth {

    add_header            Access-Control-Allow-Origin $allow_origin always;
    add_header            Access-Control-Allow-Methods "OPTIONS, GET" always;
    add_header            Access-Control-Allow-Headers "Content-Type, Accept, Authorization" always;
    add_header            Access-Control-Allow-Credentials true always;
    add_header            Access-Control-Max-Age 1728000;

    if ($request_method = "OPTIONS") {
      add_header          Access-Control-Allow-Origin $allow_origin always;
      add_header          Access-Control-Allow-Methods "OPTIONS, GET" always;
      add_header          Access-Control-Allow-Headers "Content-Type, Accept, Authorization" always;
      add_header          Access-Control-Allow-Credentials true always;
      add_header          Access-Control-Max-Age 1728000;
      add_header          Content-Type "text/plain charset=UTF-8";
      add_header          Content-Length 0;
      return              204;
    }

    # Keycloak returns 400 if no authorization header is provided, so we change it to 401 and set www-authenticate
    if ($http_authorization = "") {
      add_header          WWW-Authenticate 'Basic realm="https://my-auth-server.com"' always;
      return              401;
    }

    set                   $upstream keycloak:8080;
    proxy_pass            http://$upstream;
  }

Expected behavior

I expect the front page to prompt me for a basic auth dialog and retrieve tokens via registry server (and transitive via my keycloak)
and see all my images in the list.

Screenshots

error

System information

  • OS: Debian Buster (Docker Host)
  • Docker registry UI:
    • Version: 2.8.1 and main
    • Server: docker
    • Docker version: 23.0.1
    • Docker registry ui tag: joxit/docker-registry-ui:main
    • OS/Arch: linux/amd64
    • Tools: docker-compose

Additional context

Add any other context about the problem here.

@Joxit
Copy link
Owner

Joxit commented May 7, 2023

Hello,

Thank you for using my project and for your well detailed issue.

It seems that you are using your registry and keycloak on different domains that's right?

As I can see, when we override the query when Authorization header is empty for the 401 status code instead of keycloak's 400 status, this do not return all required headers such as Access-Control-Allow-Origin.

Can you update your nginx configuration with something like that in keycloak section ? (I added all your add_header directives).

    # Keycloak returns 400 if no authorization header is provided, so we change it to 401 and set www-authenticate
    if ($http_authorization = "") {
      add_header          Access-Control-Allow-Origin $allow_origin always;
      add_header          Access-Control-Allow-Methods "OPTIONS, GET" always;
      add_header          Access-Control-Allow-Headers "Content-Type, Accept, Authorization" always;
      add_header          Access-Control-Allow-Credentials true always;
      add_header          WWW-Authenticate 'Basic realm="https://my-auth-server.com"' always;
      return              401;
    }

I've pushed a new commit too a77103a that may also help you.

Your browser is Chrome on Mac OS right ?

@alexanderwolz
Copy link
Contributor Author

alexanderwolz commented May 7, 2023

Hi @Joxit

yes, I tested with Chrome on MacOS and I have registry and auth server on different domains.

I can confirm that putting the CORS headers into the if-block and using the withCredentials with your commit now works and I get asked for username and password in a basic auth prompt :)

Whenever you use an if-block in NGINX, the add_headers before are not executed, that's why I need to repeat myself here (or extract them into a dedicated file that gets included). See also nginx-if-is-evil - I just accidentially observed the keycloak URL with a given basic auth header in my rest client, so it showed me CORS headers in the response and hence I did not add them in the if block ..

I can also confirm that all CORS headers can be removed from the /v2/ proxy config, as I use NGINX_PROXY_PASS_URL and without having Docker-Env-CORS-Config on my registry. So the CORS headers are only needed in the /protocols/docker-v2 proxy config of Keycloak.

I can also confirm that deleting images work, I can browse all images, I can see the Dockerfiles and history.

Thank you for your quick support!!

@Joxit
Copy link
Owner

Joxit commented May 7, 2023

Glad to see everything is working now 😄.

About nginx-if-is-evil, I remembered the exact same article 😅 in my keycloak example, everything is on the same domain, so this configuration wasn't needed... 😕

Thank you for sharing your nginx configuration 😄

If you liked my work and want to support/thank me via your company, I also have a Github Sponsor Profile 🥰

@alexanderwolz
Copy link
Contributor Author

Hi @Joxit

Okay, I think we still have issue with Safari (Version 16.4). It does not open the Basic Auth Prompt here, where as in Chrome it does.. Any ideas?

error2

@alexanderwolz alexanderwolz reopened this May 7, 2023
@Joxit
Copy link
Owner

Joxit commented May 8, 2023

Hi @alexanderwolz

Sorry Safari and Apple products are beyond my scope, I will be unable to troubleshoot issues on those devices. If you succeed to fix the issue on Safari, you can share it at anytime. As long as Chrome and Firefox are working I'm fine with it 😅

I know that even if it's based on Chrome, Apple like to tweak software, maybe they add something for security ? Since we implemented the Basic Auth RFC, IDK what to do 🤔 maybe you should add charset="UTF-8" even if it's optional ?

Some (old) resources, IDK if this is still the case 🤷 :
Safari not prompting for basic authentication
Safari not prompting for credentials
Safari 5.1 and HTTP basic access authentication not working
Basic authentication not working in Safari

@alexanderwolz
Copy link
Contributor Author

alexanderwolz commented May 9, 2023

Hi @Joxit

I found a way to work in Safari.

Therefore, I just rewrote the authentication url in my proxy to run on the same domain as my registry and registry-ui.
Somehow Safari complains if you use AJAX and withCredetials that redirects (301) to another domain.

I removed all Proxy Settings from my keycloak config and moved everything in my registry config, I then set the environment variable of the registry container to that proxied url.

Now my config looks like that:

map $upstream_http_docker_distribution_api_version $docker_distribution_api_version {
  "" "registry/2.0";
}

server {

  listen                  443 ssl http2;
  listen                  [::]:443 ssl http2;
  server_name             myregistrydomain.com;

  ssl_certificate         fullchain.pem;
  ssl_certificate_key     privkey.pem;
  ssl_trusted_certificate chain.pem;

  # Docker Registry API - see https://docs.docker.com/registry/recipes/nginx/
  location /v2/ {

    # Do not allow connections from docker 1.5 and earlier
    # docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
    if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
      return 404;
    }

    # set
    add_header            Docker-Distribution-Api-Version $docker_distribution_api_version always;

    set $upstream         registry:5000;
    proxy_pass            http://$upstream;
  }

  # Docker authentication
  # see https://stackoverflow.com/questions/36582199/how-to-allow-access-via-cors-to-multiple-domains-within-nginx
  # see https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/
  location /myProxyAuthenticationEndpoint {
    # handle CORS pre- flight requests
    if ($request_method = "OPTIONS") {
      add_header          Access-Control-Allow-Origin $host always;
      add_header          Access-Control-Allow-Methods "OPTIONS, GET" always;
      add_header          Access-Control-Allow-Headers "Content-Type, Accept, Authorization" always;
      add_header          Access-Control-Allow-Credentials true always;
      add_header          Content-Type "text/plain charset=UTF-8";
      add_header          Content-Length 0;
      return              204;
    }

    # Keycloak returns 400 if no authorization header is provided, so we change it to 401 and set www-authenticate
    if ($http_authorization = "") {
      add_header          Access-Control-Allow-Origin $host always;
      add_header          Access-Control-Allow-Methods "OPTIONS, GET" always;
      add_header          Access-Control-Allow-Headers "Content-Type, Accept, Authorization" always;
      add_header          Access-Control-Allow-Credentials true always;
      add_header          WWW-Authenticate 'Basic realm="MyRealm"' always;
      return              401;
    }

    add_header            Access-Control-Allow-Origin $host always;
    add_header            Access-Control-Allow-Methods "OPTIONS, GET" always;
    add_header            Access-Control-Allow-Headers "Content-Type, Accept, Authorization" always;
    add_header            Access-Control-Allow-Credentials true always;

    set                   $upstream keycloak:8080;
    proxy_pass            http://$upstream/realms/{my-realm}/protocol/docker-v2/auth$is_args$args;
  }

  # Docker Registry UI
  location / {
    set $upstream         registry_ui:8080;
    proxy_pass            http://$upstream;
  }

}

and my compose looks kind of

version: "3.9"
services:
  registry:
    image: registry:2.8.1
    restart: unless-stopped
    environment: 
      REGISTRY_HTTP_SECRET: ${HTTP_SECRET}
      REGISTRY_STORAGE_DELETE_ENABLED: "true"
      REGISTRY_AUTH_TOKEN_REALM: ${PROXY_AUTH_URL}
      REGISTRY_AUTH_TOKEN_SERVICE: ${KEYCLOAK_CLIENT_ID}
      REGISTRY_AUTH_TOKEN_ISSUER: ${KEYCLOAK_ISSUER}
      #INFO: copy certificate from keycloak to volume  -> log in to SSO server and export trust chain
      REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /opt/certs/localhost_trust_chain.pem
   ...

Here PROXY_AUTH_URL points to https://myregistrydomain.com/myProxyAuthenticationEndpoint

@alexanderwolz
Copy link
Contributor Author

alexanderwolz commented May 9, 2023

Using the single-domain approach from above, Safari (16.4) and Firefox (113.0) on MacOS and iOS work quite well. I can also successfully login with the Docker CLI. (I have no CORS headers on my authentication server config anymore)

Only Chrome browser (112.0.5615.137 MacOS and 112.0.5615.140 on Windows 10) seems to have issues with the authentication and forces a basic auth prompt on every click. If I enter my credentials, it successfully loads resources but that's quite annoying. That happens on registry-ui:2.4.1 as well as on registry-ui:main

@Joxit: Do you have any idea what the re-authenticate could trigger on Chrome?

@Joxit
Copy link
Owner

Joxit commented May 20, 2023

Hi there, I tried your exact configuration (nginx on same domain with your configuration) and it works on Chrome...

Can you reproduce this on a read only (with only base images like alpine/debian etc) registry server/ui/keycloak only for me/debugging purpose ?

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

No branches or pull requests

2 participants