From 6812734bbd3ad98ea5122aa5d5d35389abaf7239 Mon Sep 17 00:00:00 2001 From: Anton Engelhardt Date: Fri, 6 Sep 2024 08:55:55 +0200 Subject: [PATCH] refactor: rebase logout path PR with mulitple providers PR Signed-off-by: Anton Engelhardt --- README.md | 1 + demo/configmap.yml | 1 + envoy.yaml | 1 + k8s/configmap.yml | 4 ++-- src/auth.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 5 +++-- src/discovery.rs | 16 ++++++++++++++ src/responses.rs | 2 ++ 8 files changed, 81 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 09b360d..b878515 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ The plugin is configured via the `envoy.yaml`-file. The following configuration | `id_token_header_name` | `string` | If set, this name will be used to forward the id token to the backend. | `X-Id-Token` | ❌ | | `id_token_header_prefix` | `string` | The prefix of the header, that is used to forward the id token, if empty "" is used. | `Bearer ` | ❌ | | `cookie_name` | `string` | The name of the cookie, that is used to store the session. | `oidcSession` | ✅ | +| `logout_path` | `string` | The path, that is used to logout the user. The user will be redirected to `end_session_endpoint` of the OIDC provider, if the server supports this; alternatively the user is sent to "/" | `/logout` | ✅ | | `filter_plugin_cookies` | `bool` | Whether to filter the cookies that are managed and controlled by the plugin (namely cookie_name and `nonce`). | `true` | ✅ | | `cookie_duration` | `u64` | The duration in seconds, after which the session cookie expires. | `86400` | ✅ | | `token_validation` | bool | Whether to validate the token or not. | `true` | ✅ | diff --git a/demo/configmap.yml b/demo/configmap.yml index 27e03a2..f77214e 100644 --- a/demo/configmap.yml +++ b/demo/configmap.yml @@ -51,6 +51,7 @@ data: id_token_header_prefix: "Bearer " cookie_name: "oidcSession" + logout_path: "/logout" filter_plugin_cookies: true # or false cookie_duration: 8640000 # in seconds token_validation: true # or false diff --git a/envoy.yaml b/envoy.yaml index 3daf366..b7f5693 100644 --- a/envoy.yaml +++ b/envoy.yaml @@ -41,6 +41,7 @@ static_resources: id_token_header_prefix: "Bearer " cookie_name: "oidcSession" # max. 32 characters + logout_path: "/logout" filter_plugin_cookies: true # or false cookie_duration: 8640000 # in seconds token_validation: true # or false diff --git a/k8s/configmap.yml b/k8s/configmap.yml index 2e7f848..d3c9dcf 100644 --- a/k8s/configmap.yml +++ b/k8s/configmap.yml @@ -52,8 +52,8 @@ data: id_token_header_prefix: "Bearer " cookie_name: "oidcSession" # max. 32 characters - filter_plugin_cookies: true # or false - cookie_duration: 86400 # in seconds + logout_path: "/logout" + cookie_duration: 8640000 # in seconds token_validation: true # or false aes_key: "i-am-a-forty-four-characters-long-string-key" # generate with `openssl rand -base64 32` diff --git a/src/auth.rs b/src/auth.rs index 6e84332..2ad4dd0 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -100,6 +100,17 @@ impl HttpContext for ConfiguredOidc { return Action::Continue; } + // If Path is logout route, clear cookies and redirect to base path + if path == self.plugin_config.logout_path { + match self.logout() { + Ok(action) => return action, + Err(e) => { + warn!("logout failed: {}", e); + self.show_error_page(503, "Logout failed", "Please try again, delete your cookies or contact your system administrator."); + } + } + } + // If the path matches the provider selection endpoint, redirect to the authorization endpoint // with the selected provider. if path.contains("/_wasm-oidc-plugin/provider-selection") { @@ -553,6 +564,50 @@ impl ConfiguredOidc { } } + /// Clear the cookies and redirect to the base path or `end_session_endpoint`. + fn logout(&self) -> Result { + let cookie_values = Session::make_cookie_values( + "", + "", + &self.plugin_config.cookie_name, + 0, + self.get_number_of_cookies() as u64, + ); + + let mut headers = Session::make_set_cookie_headers(&cookie_values); + + // Get session from cookie + let cookie = self.get_session_cookie_as_string()?; + let nonce = self.get_nonce()?; + let session = Session::decode_and_decrypt( + cookie, + self.plugin_config.aes_key.reveal().clone(), + nonce, + )?; + + // Get provider to use based on issuer because the end session endpoint is provider-specific + let provider = self + .open_id_providers + .iter() + .find(|provider| provider.issuer == session.issuer.clone().unwrap()) + .unwrap(); + // TODO: Error handling + + // Redirect to end session endpoint, if available (not all OIDC providers support this) + let location = match &provider.end_session_endpoint { + // if the end session endpoint is available, redirect to it + Some(url) => url.as_str(), + // else, redirect to the base path + None => "/", + }; + headers.push(("Location", location)); + headers.push(("Cache-Control", "no-cache")); + + self.send_http_response(307, headers, Some(b"Logging out...")); + + Ok(Action::Pause) + } + /// Show the auth page or redirect to the authorization endpoint. fn generate_auth_page(&self) { // If there is more than one provider, show an auth page where the user selects the provider diff --git a/src/config.rs b/src/config.rs index 40f56a5..d14b3a9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,8 +19,7 @@ use regex::Regex; // url use url::Url; -/// Struct that holds the configuration for the plugin. It is loaded from the config file -/// `envoy.yaml` +/// Struct that holds the configuration for the plugin. It is loaded from the config file `envoy.yaml` #[derive(Clone, Debug, Deserialize)] pub struct PluginConfiguration { // OpenID Connect Configuration @@ -59,6 +58,8 @@ pub struct PluginConfiguration { // Cookie settings /// The cookie name that will be used for the session cookie pub cookie_name: String, + /// The URL to logout the user + pub logout_path: String, /// Filter out the cookies created and controlled by the plugin /// If the value is true, the cookies will be filtered out pub filter_plugin_cookies: bool, diff --git a/src/discovery.rs b/src/discovery.rs index a866254..4862fab 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -90,6 +90,8 @@ pub struct OpenIdProvider { pub auth_endpoint: Url, /// The URL of the token endpoint pub token_endpoint: Url, + /// The URL of the end session endpoint + pub end_session_endpoint: Option, /// The issuer that will be used for the token request pub issuer: String, /// The public keys that will be used for the validation of the ID Token @@ -459,6 +461,7 @@ impl Context for Root { open_id_config: resolver_to_update.open_id_config.clone(), auth_endpoint: open_id_response.authorization_endpoint.clone(), token_endpoint: open_id_response.token_endpoint.clone(), + end_session_endpoint: open_id_response.end_session_endpoint.clone(), issuer: open_id_response.issuer.clone(), public_keys: keys, }); @@ -512,6 +515,19 @@ impl Root { return Err(PluginError::ConfigError("`cookie_name` is empty or not valid meaning that it contains invalid characters like ;, =, :, /, space".to_string())); } + // Logout Path + if plugin_config.logout_path.is_empty() { + return Err(PluginError::ConfigError( + "`logout_path` is empty".to_string(), + )); + } + + if !plugin_config.logout_path.starts_with('/') { + return Err(PluginError::ConfigError( + "`logout_path` does not start with a `/`".to_string(), + )); + } + // Cookie Duration if plugin_config.cookie_duration == 0 { return Err(PluginError::ConfigError( diff --git a/src/responses.rs b/src/responses.rs index 04af7f3..72badf5 100644 --- a/src/responses.rs +++ b/src/responses.rs @@ -27,6 +27,8 @@ pub struct OpenIdDiscoveryResponse { pub authorization_endpoint: Url, /// The token endpoint to exchange the code for a token pub token_endpoint: Url, + /// The URL to logout the user + pub end_session_endpoint: Option, /// The jwks uri to load the jwks response from pub jwks_uri: Url, }