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

Add support for RP-initiated OIDC logout #96

Merged
merged 1 commit into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
route443 marked this conversation as resolved.
Show resolved Hide resolved

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