Skip to content

Commit

Permalink
Add support for RP-initiated OIDC logout
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
route443 committed Jun 14, 2024
1 parent 8dad580 commit 323e6f9
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 16 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
11 changes: 8 additions & 3 deletions configure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ fi
# Build an intermediate configuration file
# File format is: <NGINX variable name><space><IdP value>
#
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
Expand Down Expand Up @@ -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:`
Expand Down
47 changes: 40 additions & 7 deletions openid_connect.js
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
}
}
11 changes: 6 additions & 5 deletions openid_connect.server_conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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;
}

Expand Down
7 changes: 7 additions & 0 deletions openid_connect_configuration.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand Down

0 comments on commit 323e6f9

Please sign in to comment.