From 323e6f9fb41ded6591a3fc543c508850ed6ab748 Mon Sep 17 00:00:00 2001 From: Ivan Ovchinnikov Date: Fri, 7 Jun 2024 08:03:28 +0000 Subject: [PATCH] Add support for RP-initiated OIDC logout Implement support for RP-initiated logout in accordance with OpenID Connect RP-Initiated Logout 1.0. Introduce "oidc_end_session_endpoint" variable to specify the "end_session_endpoint" URL. If "oidc_end_session_endpoint" is not set or is empty, the default behavior of logging out only on the NGINX side is maintained. When set, the endpoint triggers the RP-initiated logout as specified in the specification. --- README.md | 8 +++++- configure.sh | 11 ++++++-- openid_connect.js | 47 ++++++++++++++++++++++++++----- openid_connect.server_conf | 11 ++++---- openid_connect_configuration.conf | 7 +++++ 5 files changed, 68 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 22e5266..c41a978 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,10 @@ If a [refresh token](https://openid.net/specs/openid-connect-core-1_0.html#Refre Requests made to the `/logout` location invalidate both the ID token, access token and refresh token by erasing them from the key-value store. Therefore, subsequent requests to protected resources will be treated as a first-time request and send the client to the IdP for authentication. Note that the IdP may issue cookies such that an authenticated session still exists at the IdP. +#### RP-Initiated OIDC Logout + +RP-initiated logout is supported according to [OpenID Connect RP-Initiated Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0.html). This behavior is controlled by the `$oidc_end_session_endpoint` variable. + ### Multiple IdPs Where NGINX Plus is configured to proxy requests for multiple websites or applications, or user groups, these may require authentication by different IdPs. Separate IdPs can be configured, with each one matching on an attribute of the HTTP request, e.g. hostname or part of the URI path. @@ -137,11 +141,13 @@ When NGINX Plus is deployed behind another proxy, the original protocol and port * Set the **redirect URI** to the address of your NGINX Plus instance (including the port number), with `/_codexch` as the path, e.g. `https://my-nginx.example.com:443/_codexch` * Ensure NGINX Plus is configured as a confidential client (with a client secret) or a public client (with PKCE S256 enabled) * Make a note of the `client ID` and `client secret` if set + * Set the **post logout redirect URI** to the address of your NGINX Plus instance (including the port number), with `/_logout` as the path, e.g. `https://my-nginx.example.com:443/_logout` * If your IdP supports OpenID Connect Discovery (usually at the URI `/.well-known/openid-configuration`) then use the `configure.sh` script to complete configuration. In this case you can skip the next section. Otherwise: * Obtain the URL for `jwks_uri` or download the JWK file to your NGINX Plus instance * Obtain the URL for the **authorization endpoint** * Obtain the URL for the **token endpoint** + * Obtain the URL for the **end session endpoint** ## Configuring NGINX Plus @@ -165,7 +171,7 @@ Manual configuration involves reviewing the following files so that they match y * **openid_connect.server_conf** - this is the NGINX configuration for handling the various stages of OpenID Connect authorization code flow * No changes are usually required here - * Modify the `resolver` directive to match a DNS server that is capable of resolving the IdP defined in `$oidc_token_endpoint` + * Modify the `resolver` directive to match a DNS server that is capable of resolving the IdP defined in `$oidc_token_endpoint` and `$oidc_end_session_endpoint` * If using [`auth_jwt_key_request`](http://nginx.org/en/docs/http/ngx_http_auth_jwt_module.html#auth_jwt_key_request) to automatically fetch the JWK file from the IdP then modify the validity period and other caching options to suit your IdP * **openid_connect.js** - this is the JavaScript code for performing the authorization code exchange and nonce hashing diff --git a/configure.sh b/configure.sh index 17e8920..e48b0cd 100755 --- a/configure.sh +++ b/configure.sh @@ -120,7 +120,7 @@ fi # Build an intermediate configuration file # File format is: # -jq -r '. | "$oidc_authz_endpoint \(.authorization_endpoint)\n$oidc_token_endpoint \(.token_endpoint)\n$oidc_jwks_uri \(.jwks_uri)"' < /tmp/${COMMAND}_$$_json > /tmp/${COMMAND}_$$_conf +jq -r '. | "$oidc_authz_endpoint \(.authorization_endpoint)\n$oidc_token_endpoint \(.token_endpoint)\n$oidc_end_session_endpoint \(.end_session_endpoint // "")\n$oidc_jwks_uri \(.jwks_uri)"' < /tmp/${COMMAND}_$$_json > /tmp/${COMMAND}_$$_conf # Create a random value for HMAC key, adding to the intermediate configuration file echo "\$oidc_hmac_key `openssl rand -base64 18`" >> /tmp/${COMMAND}_$$_conf @@ -178,13 +178,18 @@ fi # Loop through each configuration variable echo "$COMMAND: NOTICE: Configuring $CONFDIR/openid_connect_configuration.conf" -for OIDC_VAR in \$oidc_authz_endpoint \$oidc_token_endpoint \$oidc_jwt_keyfile \$oidc_hmac_key $CLIENT_ID_VAR $CLIENT_SECRET_VAR $PKCE_ENABLE_VAR; do +for OIDC_VAR in \$oidc_authz_endpoint \$oidc_token_endpoint \$oidc_end_session_endpoint \$oidc_jwt_keyfile \$oidc_hmac_key $CLIENT_ID_VAR $CLIENT_SECRET_VAR $PKCE_ENABLE_VAR; do # Pull the configuration value from the intermediate file VALUE=`grep "^$OIDC_VAR " /tmp/${COMMAND}_$$_conf | cut -f2 -d' '` echo -n "$COMMAND: NOTICE: - $OIDC_VAR ..." + # If the value is empty, assign a default value + if [ -z "$VALUE" ]; then + VALUE="\"\"" + fi + # Find where this variable is configured - LINE=`grep -nA10 $OIDC_VAR $CONFDIR/openid_connect_configuration.conf | grep $HOSTNAME | head -1 | cut -f1 -d-` + LINE=`grep -nA10 $OIDC_VAR $CONFDIR/openid_connect_configuration.conf | grep -vE '^[0-9]+-?[[:space:]]*($|#)' | grep $HOSTNAME | head -1 | cut -f1 -d-` if [ "$LINE" == "" ]; then # Add new value LINE=`grep -n $OIDC_VAR $CONFDIR/openid_connect_configuration.conf | head -1 | cut -f1 -d:` diff --git a/openid_connect.js b/openid_connect.js index 5ef1a80..e4572eb 100644 --- a/openid_connect.js +++ b/openid_connect.js @@ -1,6 +1,6 @@ /* * JavaScript functions for providing OpenID Connect with NGINX Plus - * + * * Copyright (C) 2020 Nginx, Inc. */ var newSession = false; // Used by oidcAuth() and validateIdToken() @@ -51,7 +51,7 @@ function auth(r, afterSyncCheck) { r.return(302, r.variables.oidc_authz_endpoint + getAuthZArgs(r)); return; } - + // Pass the refresh token to the /_refresh location so that it can be // proxied to the IdP in exchange for a new id_token r.subrequest("/_refresh", "token=" + r.variables.refresh_token, @@ -266,10 +266,43 @@ function validateIdToken(r) { function logout(r) { r.log("OIDC logout for " + r.variables.cookie_auth_token); - r.variables.session_jwt = "-"; - r.variables.access_token = "-"; - r.variables.refresh_token = "-"; - r.return(302, r.variables.oidc_logout_redirect); + + // Determine if oidc_logout_redirect is a full URL or a relative path + function getLogoutRedirectUrl(base, redirect) { + return redirect.match(/^(http|https):\/\//) ? redirect : base + redirect; + } + + var logoutRedirectUrl = getLogoutRedirectUrl(r.variables.redirect_base, r.variables.oidc_logout_redirect); + + // Helper function to perform the final logout steps + function performLogout(redirectUrl) { + r.variables.session_jwt = '-'; + r.variables.access_token = '-'; + r.variables.refresh_token = '-'; + r.return(302, redirectUrl); + } + + // Check if OIDC end session endpoint is available + if (r.variables.oidc_end_session_endpoint) { + + if (!r.variables.session_jwt || r.variables.session_jwt === '-') { + if (r.variables.refresh_token && r.variables.refresh_token !== '-') { + // Renew ID token if only refresh token is available + auth(r, 0); + } else { + performLogout(logoutRedirectUrl); + return; + } + } + + // Construct logout arguments for RP-initiated logout + var logoutArgs = "?post_logout_redirect_uri=" + encodeURIComponent(logoutRedirectUrl) + + "&id_token_hint=" + encodeURIComponent(r.variables.session_jwt); + performLogout(r.variables.oidc_end_session_endpoint + logoutArgs); + } else { + // Fallback to traditional logout approach + performLogout(logoutRedirectUrl); + } } function getAuthZArgs(r) { @@ -311,5 +344,5 @@ function idpClientAuth(r) { return "code=" + r.variables.arg_code + "&code_verifier=" + r.variables.pkce_code_verifier; } else { return "code=" + r.variables.arg_code + "&client_secret=" + r.variables.oidc_client_secret; - } + } } diff --git a/openid_connect.server_conf b/openid_connect.server_conf index 13456d2..55d95a5 100644 --- a/openid_connect.server_conf +++ b/openid_connect.server_conf @@ -8,7 +8,7 @@ location = /_jwks_uri { internal; - proxy_cache jwk; # Cache the JWK Set recieved from IdP + proxy_cache jwk; # Cache the JWK Set received from IdP proxy_cache_valid 200 12h; # How long to consider keys "fresh" proxy_cache_use_stale error timeout updating; # Use old JWK Set if cannot reach IdP proxy_ssl_server_name on; # For SNI to the IdP @@ -29,9 +29,9 @@ # This location is called by the IdP after successful authentication status_zone "OIDC code exchange"; js_content oidc.codeExchange; - error_page 500 502 504 @oidc_error; + error_page 500 502 504 @oidc_error; } - + location = /_token { # This location is called by oidcCodeExchange(). We use the proxy_ directives # to construct the OpenID Connect token request, as per: @@ -68,8 +68,9 @@ location = /logout { status_zone "OIDC logout"; - add_header Set-Cookie "auth_token=; $oidc_cookie_flags"; # Send empty cookie - add_header Set-Cookie "auth_redir=; $oidc_cookie_flags"; # Erase original cookie + add_header Set-Cookie "auth_token=; $oidc_cookie_flags"; + add_header Set-Cookie "auth_nonce=; $oidc_cookie_flags"; + add_header Set-Cookie "auth_redir=; $oidc_cookie_flags"; js_content oidc.logout; } diff --git a/openid_connect_configuration.conf b/openid_connect_configuration.conf index c2b0a52..16d25e7 100644 --- a/openid_connect_configuration.conf +++ b/openid_connect_configuration.conf @@ -28,6 +28,13 @@ map $host $oidc_jwt_keyfile { default "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/certs"; } +map $host $oidc_end_session_endpoint { + # Specifies the end_session_endpoint URL for RP-initiated logout. + # If this variable is empty or not set, the default behavior is maintained, + # which logs out only on the NGINX side. + default ""; +} + map $host $oidc_client { default "my-client-id"; }