nginxinc / nginx-openid-connect

Reference implementation of OpenID Connect integration for NGINX Plus
https://www.nginx.com/products/nginx/
Other
198 stars 94 forks source link

Refresh blocked by CORS policy: No 'Access-Control-Allow-Origin' header #98

Open r300mrg opened 3 months ago

r300mrg commented 3 months ago

Using the latest code as of July 26th 2024 and configured to use Identity Provider (IdP) of Microsoft Entra ID (updating our dev server running legacy version of this code from a few years ago, not ideal but coming from a working setup at one point in time).

Initial site loads and authenticates as expected. However, after the authentication token expires the browser refresh fails to re-authenticate with the browser console providing the following CORS related issue:

Access to XMLHttpRequest at 'https://login.microsoftonline.com//oauth2/authorize?response_type=code&scope=openid+profile+email+offline_access&client_id=_uri=https://:443/_codexch&nonce=aK5DJ3mdVwzXyqsXBzWKXvncXhvm4UJpcZQ0Lj2sbk&state=0' (redirected from 'https://<mysite/page1>') from origin 'https://' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

I’ve attempted to add proxy_set_header Access-Control-Allow-Origin *; under both /_token and /_refresh in file openid_connect.server_conf, and this makes no difference.

Any thoughts or ideas how to resolve? Thanks

route443 commented 3 months ago

Hi @r300mrg , Have you tried disabling the inheritance of headers from the client request? I mean adding proxy_pass_request_headers off; to the /_refresh and /_token locations in the openid_connect.server_conf file.

r300mrg commented 3 months ago

Hi @r300mrg , Have you tried disabling the inheritance of headers from the client request? I mean adding proxy_pass_request_headers off; to the /_refresh and /_token locations in the openid_connect.server_conf file.

Hi @route443, thank you for your prompt reply. Let me try this out and get back to you. Thanks

r300mrg commented 3 months ago

@route443, I deployed the change you suggested (excluding proxy_set_header Access-Control-Allow-Origin *;) and at present my testing I'm seeing mixed results.

When the token expires its either ok, or I get the same CORS error/issue, but the pages seem to load/work without issues despite the console error and site error popups due to the console log error.

Current code:

   location = /_token {
        # This location is called by oidcCodeExchange(). We use the proxy_ directives
        # to construct the OpenID Connect token request, as per:
        #  http://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
        internal;
        proxy_ssl_server_name on; # For SNI to the IdP
        proxy_set_header      Content-Type "application/x-www-form-urlencoded";
        proxy_set_header      Authorization $arg_secret_basic;
        proxy_pass_request_headers off;  # Added for MS Entra CORS Issue
        proxy_pass            $oidc_token_endpoint;
   }

    location = /_refresh {
        # This location is called by oidcAuth() when performing a token refresh. We
        # use the proxy_ directives to construct the OpenID Connect token request, as per:
        #  https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken
        internal;
        proxy_ssl_server_name on; # For SNI to the IdP
        proxy_set_header      Content-Type "application/x-www-form-urlencoded";
        proxy_set_header      Authorization $arg_secret_basic;
        proxy_pass_request_headers off;  # Added for MS Entra CORS Issue
        proxy_pass            $oidc_token_endpoint;
    }

error.log contains the following type of errors only, but these have been present before I attempted to update the code. 2024/07/29 13:40:03 [error] 744379#744379: *249 js: OIDC refresh response did not include id_token

Any other ideas/thoughts? Thanks!

route443 commented 3 months ago

@r300mrg, so, as I understand it, the current issue is that after the id_token expires and you use the refresh token to update the tokens, you are not receiving a new id_token (based on the error in the logs). Honestly, I don't recall such behavior from MS Entra ID, meaning that if you use the openid scope, the IdP should issue a new id_token. However, I'll take a look at this, maybe something has changed in its behavior.

Looking at this issue more broadly, the refresh_token should primarily be used to update the access_token, not the id_token. In other words, the id_token is used to pass the user's identity, and its lifetime should not be tied to the session duration in your web app. However, the current peculiarity of our solution is that we "exchange" the auth_token cookie for a id_token and validate the token on each request. So, if you don't find a solution, I'd recommend increasing the lifetime of the id_token (for example, synchronizing it with the desired session duration of your application, like 8 hours) and disabling the refresh token, i.e., removing offline_access from $oidc_scopes.

r300mrg commented 3 months ago

Thank you @route443!

I'm not even going to pretend to be an expert (this is all new to me) :)

Effectively we are using the code here as it is, with the required environment values changed in openid_connect_configuration.conf with JWT file location and then locations of our sites pages which need authentication in an equivalent frontend.conf file.

I have tried to fix the js: OIDC refresh response did not include id_token error by adding &scope=$host $oidc_scopes or &scope=openid to the \_refresh proxy_set_body, as indicated as required on https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken, however this causes the page to completely fail loading when the token has expired and the page is reloaded.

Any help is greatly appreciated. Thank you

route443 commented 3 months ago

Hi @r300mrg , I checked on my side and can confirm that Entra ID reissues the id_token when using the refresh_token (after id_token expiration). So this part is working as expected.

I'd recommend not changing anything in openid_connect.server_conf, especially since managing request parameters is no longer available in openid_connect.server_conf. + try to reset $oidc_scopes to its defaults (openid+profile+email+offline_access). If this doesn't help, I'd like to take a look at your configuration if possible, particularly openid_connect_configuration.conf and the application manifest (App registrations -> your app -> manage -> manifest (JSON)). Thank you in advance!

r300mrg commented 3 months ago

Hi @route443, my configuration is effectively the same as this main repo:

configure.sh - as per latest main repo. openid_connect.js - as per latest main repo.

openid_connect.server_conf - as per latest main repo except resolver 8.8.8.8 ipv6=off valid=30s; # For DNS lookup of IdP endpoints; as IPv6 errors were seen before limiting to only IPv4 addresses.

openid_connect_configuration.conf is the same except customisation on the following:

map $host $oidc_authz_endpoint { default "https://login.microsoftonline.com/<TENNANT_ID>/oauth2/authorize"; }

map $host $oidc_token_endpoint { default "https://login.microsoftonline.com/<TENNANT_ID>/oauth2/token"; }

map $host $oidc_jwt_keyfile { default "/etc/nginx/azure.jwk"; }

map $host $oidc_client { default "<key-value01>"; }

map $host $oidc_client_secret { default "<key-value02>"; }

`map $host $oidc_hmac_key {

This should be unique for every NGINX instance/cluster

default "<key-value03>";

}`

My equivalent frontend.conf file contents are different. I've tried to re-align to some extent with proxy_set_header username $jwt_claim_sub; or proxy_set_header Authorization "Bearer $access_token"; and even proxy_set_header Authorization "Bearer $session_jwt"; as used under other locations but these cause the site to fail loading. If I leave our current frontend.conf equivalent file as is, but with all the other changes, when the token expires on the next site page refresh, I get a CORS error as initially posted and the error.log populates js: OIDC refresh response did not include id_token.

Could the issue be due to the server location setup in frontend.conf?

I don't have access to the applications manifest. I can try and get the details and re-compare with the openid_connect_configuration.conf configuration, however unfortunately I can't share this content due to security/privacy restrictions.

route443 commented 3 months ago

Hi @r300mrg ,

Could you try replacing your app (upstream) with something simpler, for example:

server {
    listen 8011;

    location / {
        return 200 "Hello, $http_username!\n";
        default_type text/plain;
    }

    location = /favicon.ico {
        access_log off;
        return 204;
    }
}

and check if you get the same behavior?

r300mrg commented 2 months ago

@route443 sorry for the delay. Other work priorities and getting access to my dev environment are delaying my investigations/testing.

I will get back once I have an update or further questions. Thank you for your patience.

r300mrg commented 2 months ago

Hi @route443,

Thank you for your patience, and apologies for the long reply that follows.

I created a basic frontend.conf as below, to test with simple html pages. This had mixed results.

# Custom log format to include the 'sub' claim in the REMOTE_USER field
log_format main_jwt_mrg '$remote_addr - $jwt_claim_sub [$time_local] "$request" $status '
                    '$body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main_jwt_mrg;
error_log /var/log/nginx/error.log debug;  # Reduce severity level as required

server {
    include conf.d/openid_connect.server_conf; # Authorization code flow and Relying Party processing

    server_name ${nginx_server_name};

    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/${nginx_server_name}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/${nginx_server_name}/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/${nginx_server_name}/chain.pem;

    location / {
        root /www/mrg-site;
        try_files $uri $uri/ /index.html =404;

        # This site is protected with OpenID Connect
        auth_jwt "" token=$session_jwt;
        error_page 401 = @do_oidc_flow;

        auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
        #auth_jwt_key_request /_jwks_uri; # Enable when using URL

        # Successfully authenticated users are proxied to the backend,
        # with 'sub' claim passed as HTTP header
        proxy_set_header username $jwt_claim_sub;

        # Bearer token is uses to authorize NGINX to access protected backend
        proxy_set_header Authorization "Bearer $access_token";

        # Intercept and redirect "401 Unauthorized" proxied responses to nginx
        # for processing with the error_page directive. Necessary if Access Token
        # can expire before ID Token.
        #proxy_intercept_errors on;

#        proxy_pass http://$server_name; # The backend site/app

    }
}

# Port 80 and redirect to https URL
server {
    listen      80;
    server_name ${nginx_server_name};
    return 301 https://$server_name$request_uri;
}

# vim: syntax=nginx

This seems to work and no visible browser errors, but also seems hit or miss. Also the error.log still contain js: OIDC refresh response did not include id_token

e.g.

2024/08/20 10:34:51 [error] 1655028#1655028: 1 js: OIDC refresh response did not include id_token 2024/08/20 10:34:52 [info] 1655028#1655028: 1 js: OIDC refresh token stored 2024/08/20 10:34:52 [info] 1655028#1655028: 1 js: OIDC success, creating session 7dc477d97050c374c1bc8e9dbf430b36 2024/08/20 10:35:42 [notice] 1655032#1655032: http file cache: /var/cache/nginx/jwk 0.000M, bsize: 4096 2024/08/20 10:35:42 [notice] 1655026#1655026: signal 17 (SIGCHLD) received from 1655032 2024/08/20 10:35:42 [notice] 1655026#1655026: cache loader process 1655032 exited with code 0 2024/08/20 10:35:42 [notice] 1655026#1655026: signal 29 (SIGIO) received 2024/08/20 10:35:51 [info] 1655027#1655027: 2 client timed out (110: Connection timed out) while waiting for request, client: , server: 0.0.0.0:443 2024/08/20 10:35:51 [info] 1655029#1655029: 3 client timed out (110: Connection timed out) while waiting for request, client: , server: 0.0.0.0:443 2024/08/20 10:37:24 [error] 1655030#1655030: 7 js: OIDC refresh response did not include id_token 2024/08/20 10:37:27 [info] 1655030#1655030: 7 js: OIDC refresh token stored 2024/08/20 10:37:27 [info] 1655030#1655030: 7 js: OIDC success, creating session 2dcca38abfe14a38c7a6a59c596bb51a

For additional background on my environment and app setup.

We run NGINX on an Azure Virtual Machine that hosts Ubuntu 20.04. Our app and NGINX all run on this Ubuntu VM. We have an Angular frontend at /machines/ that makes calls to /api/machines express node.js server) and that we specify auth for each route etc.

Our frontend.conf equivalent application file which creates the browser CORS issue and the js: OIDC refresh response did not include id_token error.log message and failure to re-authorise with Microsoft SSO without a browser reload is below:

server {
    include conf.d/openid_connect.server_conf; # Authorization code flow and Relying Party processing

    server_name ${nginx_server_name};

    location /machines {
        return 301 /machines/;
    }

    location /machines/ {

        # This site is protected with OpenID Connect
        auth_jwt "" token=$session_jwt;
        auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
        #auth_jwt_key_request /_jwks_uri; # Enable when using URL

        # Absent/invalid OpenID Connect token will (re)start auth process (including refresh)
        error_page 401 = @do_oidc_flow;

        # Successfully authenticated users are proxied to the backend

        alias  /www/site/apps-machines-client/;
        try_files $uri $uri/ /index.html =404;

        access_log /var/log/nginx/access.log main_jwt;
    }

    location /ping {
        proxy_pass http://$server_addr:3000/ping;
    }

    location /graphql {
        # This site is protected with OpenID Connect
        auth_jwt "" token=$session_jwt;
        auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
        #auth_jwt_key_request /_jwks_uri; # Enable when using URL

        # Absent/invalid OpenID Connect token will (re)start auth process (including refresh)
        error_page 401 = @do_oidc_flow;

        # Successfully authenticated users are proxied to the backend

        proxy_pass http://$server_addr:3000;
        proxy_set_header    Host            $host;
        proxy_set_header    X-Real-IP       $remote_addr;
        proxy_set_header    X-Forwarded-for $remote_addr;
        proxy_set_header    Access-Control-Allow-Origin *;
        proxy_set_header    Authorization   "Bearer $session_jwt";
        port_in_redirect off;
        proxy_redirect   http://$server_addr:3000  /;
        proxy_connect_timeout 300;

        #proxy_set_header username $jwt_claim_sub;
        #proxy_pass http://backend; # The backend site/app

        access_log /var/log/nginx/access.log main_jwt;

    }

    location /api/machines {
        # This site is protected with OpenID Connect
        auth_jwt "" token=$session_jwt;
        auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
        #auth_jwt_key_request /_jwks_uri; # Enable when using URL

        # Absent/invalid OpenID Connect token will (re)start auth process (including refresh)
        error_page 401 = @do_oidc_flow;

        # Successfully authenticated users are proxied to the backend

        rewrite ^/api/machines(/.*)$ $1 break; # Strip '/api/machines' prefix before proxying

        proxy_pass http://$server_addr:7000;
        proxy_set_header    Host            $host;
        proxy_set_header    X-Real-IP       $remote_addr;
        proxy_set_header    X-Forwarded-for $remote_addr;
        proxy_set_header    Access-Control-Allow-Origin *;
        proxy_set_header    Authorization   "Bearer $session_jwt";
        proxy_pass_header Authorization;
        port_in_redirect off;
        proxy_redirect   http://$server_addr:7000  /;
        proxy_connect_timeout 300;

        #proxy_set_header username $jwt_claim_sub;
        #proxy_pass http://backend; # The backend site/app

        access_log /var/log/nginx/access.log main_jwt;

    }

    location /api/machines/sse {
        # This site is protected with OpenID Connect
        auth_jwt "" token=$session_jwt;
        auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
        #auth_jwt_key_request /_jwks_uri; # Enable when using URL

        # Absent/invalid OpenID Connect token will (re)start auth process (including refresh)
        error_page 401 = @do_oidc_flow;

        # Successfully authenticated users are proxied to the backend

        rewrite ^/api/machines(/.*)$ $1 break; # Strip '/api/machines' prefix before proxying

        proxy_pass http://$server_addr:7000;
        proxy_set_header    Host            $host;
        proxy_set_header    X-Real-IP       $remote_addr;
        proxy_set_header    X-Forwarded-for $remote_addr;
        proxy_set_header    Access-Control-Allow-Origin *;
        proxy_set_header    Authorization   "Bearer $session_jwt";

        # Required headers for SSE
        proxy_set_header X-Accel-Buffering "no";
        proxy_buffering off;

        # Additional configuration for improved handling of connections
        proxy_send_timeout 600;
        proxy_read_timeout 600;
        send_timeout 600;

        proxy_pass_header Authorization;
        port_in_redirect off;
        proxy_redirect   http://$server_addr:7000  /;
        proxy_connect_timeout 300;

        access_log /var/log/nginx/access.log main_jwt;

    }

    location /api/machines/ping {
        proxy_pass http://$server_addr:7000/ping;
    }

    location /api/user {

        # This site is protected with OpenID Connect
        auth_jwt "" token=$session_jwt;
        auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
        #auth_jwt_key_request /_jwks_uri; # Enable when using URL

        # Absent/invalid OpenID Connect token will (re)start auth process (including refresh)
        error_page 401 = @do_oidc_flow;

        # Successfully authenticated users are proxied to the backend

        proxy_pass http://$server_addr:3000;
        proxy_set_header    Host            $host;
        proxy_set_header    X-Real-IP       $remote_addr;
        proxy_set_header    X-Forwarded-for $remote_addr;
        proxy_set_header    Access-Control-Allow-Origin *;
        proxy_set_header    Authorization   "Bearer $session_jwt";
        port_in_redirect off;
        proxy_redirect   http://$server_addr:3000  /api/user;
        proxy_connect_timeout 300;

        #proxy_set_header username $jwt_claim_sub;
        #proxy_pass http://backend; # The backend site/app

        access_log /var/log/nginx/access.log main_jwt;

    }

    location / {
        # This site is protected with OpenID Connect
        auth_jwt "" token=$session_jwt;
        auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
        #auth_jwt_key_request /_jwks_uri; # Enable when using URL

        # Absent/invalid OpenID Connect token will (re)start auth process (including refresh)
        error_page 401 = @do_oidc_flow;

        # Successfully authenticated users are proxied to the backend

        root   /www/site/apps-machines-client;
        index  index.html index.htm;

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   /usr/share/nginx/html;
        }
    }

    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/${nginx_server_name}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/${nginx_server_name}/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/${nginx_server_name}/chain.pem;
}

server {
    if ($host = ${nginx_server_name}) {
        return 301 https://$host$request_uri;
    }

    listen 80 default_server;
    server_name ${nginx_server_name};
    return 404;
}

One difference in our frontend.conf file to the current main branch is:

#Existing
        proxy_set_header    Authorization   "Bearer $session_jwt";

#New
        # Bearer token is uses to authorize NGINX to access protected backend
        #proxy_set_header Authorization "Bearer $access_token";

Our app has been built on a legacy openid-connect "Bearer $session_jwt" header passed and not the current Bearer $access_token, but even with the older existing openid-connect config files the site works until the authorisation token expires and the SSO refresh fails.

Any thoughts or recommendations?

Many thanks

r300mrg commented 2 months ago

Hi @route443,

Just an update on our investigations and updates from my side.

The error log entry js: OIDC refresh response did not include id_token looks to have been fixed.

Our configuration was still using the the v1.0 token Microsoft Entra ID Oauth links for $host $oidc_authz_endpoint and $oidc_token_endpoint.

e.g. https://login.microsoftonline.com/{tennant-id}/oauth2/authorize and not https://login.microsoftonline.com/{tennant-id}/oauth2/v2.0/authorize and our Microsoft App registrations settings had conflicting "accessTokenAcceptedVersion": 2,.

We still have the CORS initial browser error, but are looking to re-align the "Bearer $session_jwt" and Bearer $access_token settings in our application.