Skip to main content

wasm_oidc_plugin/
auth.rs

1// base64
2use base64::{engine::general_purpose::STANDARD_NO_PAD as base64engine, Engine as _};
3
4// duration
5use std::time::Duration;
6
7// jwt
8use jwt_simple::prelude::*;
9
10// log
11use log::{debug, warn};
12
13// std
14use std::sync::Arc;
15use std::vec;
16
17// proxy-wasm
18use proxy_wasm::traits::*;
19use proxy_wasm::types::*;
20
21// url
22use url::{form_urlencoded, Url};
23
24use crate::config::V2PluginConfiguration;
25use crate::discovery::OpenIdProvider;
26use crate::error::PluginError;
27use crate::html;
28use crate::responses::{CodeCallback, ProviderSelectionCallback};
29use crate::session;
30use crate::session::{AuthorizationState, Session};
31
32/// The `OidcHttpContext` is the main filter struct and responsible for the OpenID authentication flow.
33/// Requests arriving are checked for a valid cookie. If the cookie is valid, the request is
34/// forwarded. If the cookie is not valid, the user is redirected to the authorization endpoint.
35pub struct OidcHttpContext {
36    /// The configuration of the filter which mainly contains the open id configuration and the
37    /// keys to validate the JWT
38    pub open_id_providers: Vec<OpenIdProvider>,
39    /// Plugin configuration parsed from the envoy configuration
40    pub plugin_config: Arc<V2PluginConfiguration>,
41    /// Token id of the current request
42    pub token_id: Option<u32>,
43    /// ID of the current request
44    pub request_id: String,
45}
46
47/// The context is used to process incoming HTTP requests when the filter is configured.
48///
49/// ## Flow
50///
51/// * Check for excluded hosts, paths or URLs and forward the request if excluded
52/// * Check for health route and return 200
53/// * Check for logout route and clear cookies
54/// * Check for provider selection route and redirect to authorization endpoint
55/// * Check for code callback route and exchange code for token
56/// * Validate cookie and forward request if valid
57/// * If the cookie is not valid, generate the auth page or redirect to the authorization endpoint.
58///
59impl HttpContext for OidcHttpContext {
60    /// This function is called when the request headers are received.
61    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
62        // Get the host, path and scheme from the request headers
63        let host = self.get_host().unwrap_or_default();
64        let path = self.get_http_request_header(":path").unwrap_or_default();
65        let query = path.split('?').nth(1).unwrap_or("");
66        let scheme = self
67            .get_http_request_header(":scheme")
68            .unwrap_or("http".to_string());
69        let url = Url::parse(&format!("{scheme}://{host}{path}"))
70            .unwrap_or(Url::parse("http://example.com").unwrap());
71        debug!("url: {}", url);
72
73        // Get x-request-id
74        if let Some(x_request_id) = self.get_http_request_header("x-request-id") {
75            self.request_id = x_request_id
76        }
77
78        debug!("x-request-id: {}", self.request_id);
79
80        // If the request should be excluded, continue
81        if self.request_should_be_excluded(&host, &path, &url) {
82            return Action::Continue;
83        }
84
85        // Compare against `url.path()` (no query) with `==` for exact plugin endpoints.
86        // Prefer that over `:path` / `starts_with`, which either miss `?query` or match longer paths.
87        let request_path = url.path();
88
89        // Health check
90        if request_path == "/plugin-health" {
91            self.send_http_response(200, vec![], Some(b"OK"));
92            return Action::Pause;
93        }
94
95        // If Path is logout route, clear cookies and redirect to base path
96        // (or the provider end-session endpoint when the session is still readable)
97        if request_path == self.plugin_config.logout_path {
98            if !self.is_same_origin_get() {
99                self.send_http_response(403, vec![], Some(b"Forbidden"));
100                return Action::Pause;
101            }
102            return self.logout();
103        }
104
105        // If the path matches the clear-cookies endpoint, clear cookies and redirect to base path.
106        // This is used from the error page to allow users to reset their session without triggering
107        // a full logout at the OIDC provider.
108        if request_path == "/_wasm-oidc-plugin/clear-cookies" {
109            if !self.is_same_origin_get() {
110                self.send_http_response(403, vec![], Some(b"Forbidden"));
111                return Action::Pause;
112            }
113            return self.clear_cookies();
114        }
115
116        // If the path matches the provider selection endpoint, redirect to the authorization endpoint
117        // with the selected provider.
118        if request_path == "/_wasm-oidc-plugin/provider-selection" {
119            match self.provider_selection(query) {
120                Ok(_) => return Action::Pause,
121                Err(e) => {
122                    warn!(
123                        "provider selection failed for request {} with error: {}",
124                        self.request_id, e
125                    );
126                    self.show_error_page(503, "Provider selection failed", "Please try again, delete your cookies or contact your system administrator with the request id!", true);
127                    return Action::Pause;
128                }
129            }
130        }
131
132        // If the path matches one of the `redirect_uri`s, exchange the code for a token
133        if self
134            .open_id_providers
135            .iter()
136            .any(|provider| request_path == provider.open_id_config.redirect_uri.path())
137        {
138            match self.exchange_code_for_token(path) {
139                Ok(_) => return Action::Pause,
140                Err(e) => {
141                    warn!(
142                        "token exchange failed for request {} with error: {}",
143                        self.request_id, e
144                    );
145                    self.show_error_page(503, "Token exchange failed", "Please try again, delete your cookies or contact your system administrator with the request id!", true);
146                }
147            }
148            return Action::Pause;
149        }
150
151        // Else, validate the cookie and forward the request if the authorization state is valid
152        match self.validate_cookie() {
153            Err(e) => match e {
154                // Do not show an error page for these errors
155                PluginError::SessionCookieNotFoundError => {
156                    debug!("session cookie not found, continuing with authentication");
157                }
158                PluginError::NonceCookieNotFoundError => {
159                    debug!("nonce cookie not found, continuing with authentication");
160                }
161                PluginError::TokenValidationError(_) => {
162                    debug!("token validation failed, continuing with authentication");
163                }
164                PluginError::AuthorizationStateNotFoundError => {
165                    debug!("authorization state not found, result of an unfinished authentication process, continuing with authentication");
166                }
167                _ => {
168                    warn!(
169                        "cookie validation failed for request {} with error: {}",
170                        self.request_id, e
171                    );
172                    self.show_error_page(503, "Cookie validation failed", "Please try again, delete your cookies or contact your system administrator with the request id!", true);
173                }
174            },
175            Ok(auth_state) => {
176                // Append headers
177                self.append_headers(&auth_state);
178                // Filter proxy cookies
179                self.filter_proxy_cookies();
180
181                // Allow request to pass
182                return Action::Continue;
183            }
184        }
185
186        // If any previous condition was not met, it means that the cookie is not valid or not present.
187        // Then, show the auth page or redirect to the authorization endpoint (depending on the number of providers)
188        self.generate_auth_page();
189
190        // Pause the request
191        Action::Pause
192    }
193}
194
195/// This context is used to process HTTP responses from the token endpoint.
196impl Context for OidcHttpContext {
197    /// This function catches the response from the token endpoint. We use an inner function to
198    /// handle errors more easily.
199    fn on_http_call_response(&mut self, token_id: u32, _: usize, body_size: usize, _: usize) {
200        // Store the token in the cookie
201        match self.store_token_in_cookie(token_id, body_size) {
202            Ok(_) => {
203                debug!("token stored in cookie");
204            }
205            Err(e) => {
206                warn!(
207                    "storing token in cookie failed for request {} with error: {}",
208                    self.request_id, e
209                );
210                // Send a 503 if storing the token in the cookie failed
211                self.show_error_page(
212                    503,
213                    "Storing Token in Cookie failed",
214                    "Please try again, delete your cookies or contact your system administrator with the request id!",
215                    true,
216                );
217            }
218        }
219    }
220}
221
222/// Helper functions for the `OidcHttpContext`` struct.
223impl OidcHttpContext {
224    /// Check if the request is excluded.
225    ///
226    /// ## Arguments
227    /// * `host` - The host of the request
228    /// * `path` - The path of the request
229    /// * `url` - The URL of the request
230    ///
231    /// ## Returns
232    /// * `true` - If the request is excluded
233    /// * `false` - If the request is not excluded
234    ///
235    fn request_should_be_excluded(&self, host: &str, path: &str, url: &Url) -> bool {
236        // If the host is one of the exclude hosts, forward the request
237        if self
238            .plugin_config
239            .exclude_hosts
240            .iter()
241            .any(|x| x.is_match(host))
242        {
243            debug!("host {host} is excluded, forwarding request.");
244            self.filter_proxy_cookies();
245            return true;
246        }
247
248        // If the path is one of the exclude paths, forward the request
249        if self
250            .plugin_config
251            .exclude_paths
252            .iter()
253            .any(|x| x.is_match(path))
254        {
255            debug!("path {path} is excluded, forwarding request.");
256            self.filter_proxy_cookies();
257            return true;
258        }
259
260        // If the URL is one of the exclude URLs, forward the request
261        if self
262            .plugin_config
263            .exclude_urls
264            .iter()
265            .any(|x| x.is_match(url.as_str()))
266        {
267            debug!("url {url} is excluded, forwarding request.");
268            self.filter_proxy_cookies();
269            return true;
270        }
271
272        false
273    }
274
275    /// Check if the cookie is valid and if the token is valid.
276    ///
277    /// ## Returns
278    /// * Ok(AuthorizationState) - If the cookie is valid and the token is valid
279    /// * Err(PluginError) - If the cookie is not valid or the token is not valid
280    fn validate_cookie(&self) -> Result<AuthorizationState, PluginError> {
281        // Get cookie and nonce
282        let cookie = self.get_session_cookie_as_string()?;
283        let nonce = self.get_nonce()?;
284
285        // Try to parse and decrypt the cookie and handle the result
286        match Session::decode_and_decrypt(
287            cookie,
288            self.plugin_config.aes_key.reveal().clone(),
289            nonce,
290        ) {
291            // If the cookie cannot be parsed, this function returns an error
292            Err(e) => Err(PluginError::CookieValidationError(e.to_string())),
293            // If the cookie can be parsed, this means that the cookie is trusted because modifications would have
294            // corrupted the encrypted state. Token validation is only performed if the configuration option is set.
295            Ok(session) => {
296                // Only validate the token if the configuration option is set
297                match self.plugin_config.token_validation {
298                    true => {
299                        // Get authorization state from session
300                        let auth_state = match session.authorization_state {
301                            Some(auth_state) => auth_state,
302                            None => {
303                                return Err(PluginError::AuthorizationStateNotFoundError);
304                            }
305                        };
306
307                        // Validate token
308                        match self.validate_token(&auth_state.id_token, &session.issuer) {
309                            // If the token is valid, this filter passes the request
310                            Ok(_) => {
311                                debug!("token is valid, passing request");
312                                Ok(auth_state)
313                            }
314                            // If the token is invalid, the error is returned and the user is redirected to the auth page
315                            Err(e) => Err(PluginError::TokenValidationError(e.into())),
316                        }
317                    }
318                    false => match session.authorization_state {
319                        Some(auth_state) => Ok(auth_state),
320                        // If no authorization state is found, return an error
321                        None => Err(PluginError::CookieValidationError(
322                            "No authorization state found".to_string(),
323                        )),
324                    },
325                }
326            }
327        }
328    }
329
330    /// Validate the token using the JWT library and a given issuer.
331    /// This function checks for the given issuer and audience and verifies the signature with the
332    /// public keys loaded from the JWKs endpoint.
333    ///
334    /// ## Arguments
335    /// * `token` - The token to validate
336    /// * `issuer` - The issuer to validate the token against
337    ///
338    /// ## Returns
339    ///
340    /// A result with the following variants:
341    /// * Ok(()) - If the token is valid
342    /// * Err(PluginError) - If the token is invalid
343    ///
344    fn validate_token(&self, token: &str, issuer: &str) -> Result<(), PluginError> {
345        // Get provider to use based on issuer
346        let provider_to_use = match self
347            .open_id_providers
348            .iter()
349            .find(|provider| provider.issuer == issuer)
350        {
351            Some(provider) => provider,
352            None => {
353                return Err(PluginError::ProviderNotFoundError(
354                    "unknown issuer".to_string(),
355                ));
356            }
357        };
358
359        // Define allowed issuers and audiences
360        let mut allowed_issuers = HashSet::new();
361        allowed_issuers.insert(provider_to_use.issuer.clone());
362        let mut allowed_audiences = HashSet::new();
363        allowed_audiences.insert(provider_to_use.open_id_config.audience.clone());
364
365        // Define verification options
366        let verification_options = VerificationOptions {
367            allowed_issuers: Some(allowed_issuers),
368            allowed_audiences: Some(allowed_audiences),
369            ..Default::default()
370        };
371
372        // Iterate over all public keys of the provider
373        for public_key in provider_to_use.public_keys.iter() {
374            // Perform the validation
375            let validation_result = public_key.verify_token(token, verification_options.clone());
376
377            // Check if the token is valid, the aud and iss are correct and the signature is valid.
378            match validation_result {
379                Ok(_) => return Ok(()),
380                Err(e) => {
381                    debug!("token validation failed: {:?}", e);
382                    continue;
383                }
384            }
385        }
386        // If no key worked for validation, return an error
387        Err(PluginError::NoKeyError)
388    }
389
390    /// Redirect to the authorization endpoint with the selected provider.
391    ///
392    /// ## Arguments
393    ///
394    /// * `query` - The query string from the provider selection callback
395    ///
396    fn provider_selection(&mut self, query: &str) -> Result<(), PluginError> {
397        // Deserialize the query into a struct
398        let provider_selection_callback =
399            serde_urlencoded::from_str::<ProviderSelectionCallback>(query)?;
400
401        // Find the provider to authorize with
402        let provider_to_authorize_with = self
403            .open_id_providers
404            .iter()
405            .find(|provider| {
406                provider.open_id_config.name == provider_selection_callback.authorize_with_provider
407            })
408            .ok_or(PluginError::ProviderNotFoundError(
409                "unknown provider".to_string(),
410            ))?;
411
412        // Redirect to the authorization endpoint
413        self.redirect_to_authorization_endpoint(
414            provider_to_authorize_with,
415            Some(provider_selection_callback.return_to),
416        );
417        Ok(())
418    }
419
420    /// Exchange the code for a token using the token endpoint.
421    /// This function is called when the user is redirected back to the callback URL.
422    /// The code is extracted from the URL and exchanged for a token using the token endpoint.
423    ///
424    /// ## Arguments
425    ///
426    /// * `path` - The path of the request
427    ///
428    /// ## Returns
429    ///
430    /// * Ok(()) - If the token is exchanged successfully
431    /// * Err(PluginError) - If the token exchange fails
432    fn exchange_code_for_token(&mut self, path: String) -> Result<(), PluginError> {
433        debug!("received request for OpenID callback");
434
435        // Get Query String from URL
436        let query = path.split('?').next_back().unwrap_or_default();
437        debug!("query: {query}");
438
439        // Get state from query
440        let callback_params = serde_urlencoded::from_str::<CodeCallback>(query)?;
441
442        // Get cookie and nonce
443        let encoded_cookie = self.get_session_cookie_as_string()?;
444        let encoded_nonce = self.get_nonce()?;
445
446        // Get session
447        let session = Session::decode_and_decrypt(
448            encoded_cookie,
449            self.plugin_config.aes_key.reveal().clone(),
450            encoded_nonce,
451        )?;
452
453        // Get issuer from session
454        let issuer = session.issuer.clone();
455
456        // Get provider to use based on issuer
457        let provider_to_use = match self
458            .open_id_providers
459            .iter()
460            .find(|provider| provider.issuer == issuer)
461        {
462            Some(provider) => provider,
463            None => {
464                return Err(PluginError::ProviderNotFoundError(
465                    "unknown issuer".to_string(),
466                ));
467            }
468        };
469
470        // Get state and code from query
471        let code = callback_params.code;
472        debug!("authorization code: {code}");
473        let state = callback_params.state;
474        debug!("client state: {state}");
475        debug!("cookie state: {}", session.state);
476
477        // Compare state
478        if state != session.state {
479            return Err(PluginError::StateMismatchError);
480        }
481
482        // Encode client_id and client_secret and build the Authorization header using base64encoding
483        let auth = format!(
484            "Basic {}",
485            base64engine.encode(
486                format!(
487                    "{}:{}",
488                    provider_to_use.open_id_config.client_id,
489                    provider_to_use.open_id_config.client_secret.reveal()
490                )
491                .as_bytes()
492            )
493        );
494
495        // Get code verifier from cookie
496        let code_verifier = session.code_verifier;
497
498        // Build the request body for the token endpoint
499        let data = form_urlencoded::Serializer::new(String::new())
500            .append_pair("grant_type", "authorization_code")
501            .append_pair("code_verifier", &code_verifier)
502            .append_pair("code", &code)
503            .append_pair(
504                "redirect_uri",
505                provider_to_use.open_id_config.redirect_uri.as_str(),
506            )
507            .append_pair("state", &state)
508            .finish();
509
510        // Dispatch request to token endpoint using built-in envoy function.
511        let token_authority = provider_to_use
512            .token_endpoint
513            .host_str()
514            .unwrap_or(provider_to_use.open_id_config.authority.as_str());
515
516        debug!("sending data to token endpoint: {data}");
517        match self.dispatch_http_call(
518            &provider_to_use.open_id_config.upstream_cluster,
519            vec![
520                (":method", "POST"),
521                (":path", provider_to_use.token_endpoint.path()),
522                (":authority", token_authority),
523                ("Authorization", &auth),
524                ("Content-Type", "application/x-www-form-urlencoded"),
525            ],
526            Some(data.as_bytes()),
527            vec![],
528            Duration::from_secs(10),
529        ) {
530            // If the request fails, this filter logs the error and pauses the request
531            Err(_) => Err(PluginError::DispatchError),
532            // If the request is dispatched successfully, this filter pauses the request
533            Ok(id) => {
534                self.token_id = Some(id);
535                Ok(())
536            }
537        }
538    }
539
540    /// Store the token from the token response in an encrypted cookie.
541    ///
542    /// ## Arguments
543    ///
544    /// * `token_id` - The token id of the response
545    /// * `body_size` - The size of the response body
546    ///
547    /// ## Returns
548    ///
549    /// * Ok(()) - If the token is stored in the cookie successfully
550    /// * Err(PluginError) - If the token could not be stored in the cookie
551    fn store_token_in_cookie(
552        &mut self,
553        token_id: u32,
554        body_size: usize,
555    ) -> Result<(), PluginError> {
556        // Assess token id
557        if self.token_id != Some(token_id) {
558            return Err(PluginError::TokenIdMismatchError);
559        }
560
561        // Check if the response is valid. If its not 200, investigate the response and log the error.
562        if self.get_http_call_response_header(":status") != Some("200".to_string()) {
563            // Get body of response
564            match self.get_http_call_response_body(0, body_size) {
565                // If no body is found, log the error
566                None => return Err(PluginError::NoBodyError),
567                Some(body) => {
568                    // Parse body
569                    match String::from_utf8(body) {
570                        Ok(decoded) => return Err(PluginError::TokenResponseFormatError(decoded)),
571                        // If parsing fails, log the error
572                        Err(e) => return Err(PluginError::Utf8Error(e)),
573                    }
574                }
575            }
576        }
577
578        // Previously we checked for the status code and the body, so we can assume that the response is valid.
579        match self.get_http_call_response_body(0, body_size) {
580            // If no body is found, return the error
581            None => Err(PluginError::CookieStoreError(
582                "No body in response".to_string(),
583            )),
584            Some(body) => {
585                // Get nonce and cookie
586                let encoded_cookie = self.get_session_cookie_as_string()?;
587                let encoded_nonce = self.get_nonce()?;
588
589                // Get session from cookie
590                let mut session = Session::decode_and_decrypt(
591                    encoded_cookie,
592                    self.plugin_config.aes_key.reveal().clone(),
593                    encoded_nonce,
594                )?;
595
596                // Parse authorization state from token response
597                let authorization_state = serde_json::from_slice::<AuthorizationState>(&body)?;
598                debug!("authorization state: {authorization_state:?}");
599
600                // Add authorization state to session
601                session.authorization_state = Some(authorization_state);
602
603                // Re-encrypt and re-encode session
604                let (new_session, new_nonce) =
605                    session.encrypt_and_encode(self.plugin_config.aes_key.reveal().clone())?;
606
607                // Build cookie values
608                let set_cookie_values = Session::make_cookie_values(
609                    &new_session,
610                    &new_nonce,
611                    self.plugin_config.cookie_name.as_str(),
612                    self.plugin_config.cookie_duration_in_s,
613                );
614
615                // Build cookie headers
616                let mut headers = Session::make_set_cookie_headers(&set_cookie_values);
617
618                // Set the location header to the original path
619                let location_header = ("Location", session.original_path.as_str());
620                headers.push(location_header);
621
622                // Redirect back to the original URL and set the cookie.
623                self.send_http_response(307, headers, Some(b"Redirecting..."));
624                Ok(())
625            }
626        }
627    }
628
629    /// Returns true when the request is a same-origin (or non-browser) GET.
630    ///
631    /// Used to mitigate logout / cookie-reset CSRF from cross-site navigations.
632    /// `Sec-Fetch-Site` is absent in older browsers and non-browser clients; those are
633    /// allowed. Modern browsers send `cross-site` for attacker-driven top-level GETs.
634    fn is_same_origin_get(&self) -> bool {
635        let method = self.get_http_request_header(":method").unwrap_or_default();
636        if !method.eq_ignore_ascii_case("GET") {
637            return false;
638        }
639
640        match self.get_http_request_header("sec-fetch-site").as_deref() {
641            None | Some("same-origin") | Some("none") => true,
642            Some(_) => false,
643        }
644    }
645
646    /// Clear the session cookies and redirect to the base path or `end_session_endpoint`.
647    ///
648    /// Always clears cookies via `Set-Cookie` with `Max-Age=0`, even when the session is
649    /// missing or undecryptable (HttpOnly cookies cannot be cleared from the browser).
650    fn logout(&self) -> Action {
651        let cookie_name = &self.plugin_config.cookie_name;
652        let num_parts = self
653            .get_cookie(&format!("{cookie_name}-parts"))
654            .and_then(|value| value.parse().ok())
655            .unwrap_or(1);
656        let cookie_values = Session::clear_cookie_values(cookie_name, num_parts);
657        let mut headers = Session::make_set_cookie_headers(&cookie_values);
658
659        // Prefer IdP end-session redirect when the session is still readable; otherwise go home.
660        let location = self
661            .get_session_cookie_as_string()
662            .ok()
663            .and_then(|cookie| {
664                let nonce = self.get_nonce().ok()?;
665                Session::decode_and_decrypt(
666                    cookie,
667                    self.plugin_config.aes_key.reveal().clone(),
668                    nonce,
669                )
670                .ok()
671            })
672            .and_then(|session| {
673                self.open_id_providers
674                    .iter()
675                    .find(|provider| provider.issuer == session.issuer)
676                    .and_then(|provider| provider.end_session_endpoint.as_ref())
677                    .map(|url| url.as_str().to_string())
678            })
679            .unwrap_or_else(|| "/".to_string());
680
681        headers.push(("Location", location.as_str()));
682        headers.push(("Cache-Control", "no-cache"));
683
684        self.send_http_response(307, headers, Some(b"Logging out..."));
685
686        Action::Pause
687    }
688
689    /// Clear the session cookies and redirect to the base path.
690    ///
691    /// Unlike [`logout`], this does **not** redirect to the OIDC provider's
692    /// end-session endpoint. It is meant for error-recovery: the user's cookies
693    /// are corrupt or invalid, so we just wipe them and let re-authentication
694    /// happen on the next request.
695    fn clear_cookies(&self) -> Action {
696        let cookie_name = &self.plugin_config.cookie_name;
697        let num_parts = self
698            .get_cookie(&format!("{cookie_name}-parts"))
699            .and_then(|value| value.parse().ok())
700            .unwrap_or(1);
701        let cookie_values = Session::clear_cookie_values(cookie_name, num_parts);
702        let mut headers = Session::make_set_cookie_headers(&cookie_values);
703
704        headers.push(("Location", "/"));
705        headers.push(("Cache-Control", "no-cache"));
706
707        self.send_http_response(307, headers, Some(b"Clearing cookies..."));
708
709        Action::Pause
710    }
711
712    /// Show the auth page or redirect to the authorization endpoint.
713    fn generate_auth_page(&self) {
714        // If there is more than one provider, show an auth page where the user selects the provider
715        if self.open_id_providers.len() > 1 {
716            debug!("no session cookie found or invalid, showing auth page");
717
718            // Grab the original path and encode it
719            let original_path = self
720                .get_http_request_header(":path")
721                .unwrap_or("/".to_string());
722            let original_path_encoded = base64engine.encode(original_path.as_bytes());
723
724            let mut urls = vec![];
725            let mut provider_cards = String::new();
726
727            // Create a card for each provider which sends the user back to the plugin with the selected provider
728            for open_id_provider in self.open_id_providers.iter() {
729                let url = format!(
730                    "/_wasm-oidc-plugin/provider-selection?authorize_with_provider={}&return_to={}",
731                    open_id_provider.open_id_config.name, original_path_encoded
732                );
733                urls.push(url.clone());
734
735                let provider_card = html::provider_card(
736                    &url,
737                    open_id_provider.open_id_config.name.as_str(),
738                    open_id_provider.open_id_config.image.as_str(),
739                );
740                provider_cards.push_str(&provider_card);
741            }
742
743            let headers = vec![("cache-control", "no-cache"), ("content-type", "text/html")];
744
745            // Show the auth page
746            self.send_http_response(
747                200,
748                headers,
749                Some(html::auth_page_html(provider_cards).as_bytes()),
750            );
751        } else if let Some(provider) = self.open_id_providers.first() {
752            // If there is only one provider, redirect the user to the authorization endpoint right away
753            debug!("no session cookie found or invalid, redirecting to authorization endpoint");
754            self.redirect_to_authorization_endpoint(provider, None);
755        } else {
756            warn!(
757                "no open id providers configured for request {}",
758                self.request_id
759            );
760            self.show_error_page(
761                503,
762                "No providers configured",
763                "Please contact your system administrator with the request id!",
764                false,
765            );
766        }
767    }
768
769    /// Redirect to the `authorization_endpoint` by sending a HTTP response with a 307 status code.
770    /// This function generates a PKCE code verifier and challenge, creates a session struct, encrypts
771    /// and encodes the session, and sets the cookie headers.
772    ///
773    /// ## Arguments
774    ///
775    /// * `open_id_provider` - The OpenID provider to redirect to
776    /// * `return_to` - The original path to redirect to after login
777    pub fn redirect_to_authorization_endpoint(
778        &self,
779        open_id_provider: &OpenIdProvider,
780        return_to: Option<String>,
781    ) -> Action {
782        // The `original_path` is the path to which the user should be redirected after login. and it can be
783        // passed as a query parameter. If the `return_to` parameter is not set, the original path is the current path
784        // (this is the case when there is only one provider).
785        let original_path: String = match return_to {
786            Some(return_to) => match base64engine.decode(return_to.as_bytes()) {
787                Ok(decoded) => match String::from_utf8(decoded) {
788                    Ok(decoded) => decoded,
789                    Err(_) => "/".to_string(),
790                },
791                Err(_) => "/".to_string(),
792            },
793            None => self
794                .get_http_request_header(":path")
795                .unwrap_or("/".to_string()),
796        };
797
798        // Generate PKCE code verifier and challenge
799        let pkce_verifier = pkce::code_verifier(128);
800        let pkce_verifier_string = String::from_utf8_lossy(&pkce_verifier).into_owned();
801        let pkce_challenge = pkce::code_challenge(&pkce_verifier);
802
803        // Generate state
804        let state_string = String::from_utf8_lossy(&pkce::code_verifier(128)).into_owned();
805
806        // Create session struct and encrypt it
807        let (session, nonce) = session::Session {
808            issuer: open_id_provider.issuer.clone(),
809            authorization_state: None,
810            original_path,
811            code_verifier: pkce_verifier_string,
812            state: state_string.clone(),
813        }
814        .encrypt_and_encode(self.plugin_config.aes_key.reveal().clone())
815        .expect("session cookie could not be created");
816
817        // Build cookie values
818        let set_cookie_values = Session::make_cookie_values(
819            &session,
820            &nonce,
821            self.plugin_config.cookie_name.as_str(),
822            self.plugin_config.cookie_duration_in_s,
823        );
824
825        // Build cookie headers
826        let mut headers = Session::make_set_cookie_headers(&set_cookie_values);
827
828        let claims =
829            serde_json::to_string(&open_id_provider.open_id_config.claims).unwrap_or_default();
830
831        // Build URL
832        let location = match Url::parse_with_params(
833            open_id_provider.auth_endpoint.as_str(),
834            &[
835                ("response_type", "code"),
836                ("code_challenge", &pkce_challenge),
837                ("code_challenge_method", "S256"),
838                ("state", &state_string),
839                ("client_id", &open_id_provider.open_id_config.client_id),
840                (
841                    "redirect_uri",
842                    open_id_provider.open_id_config.redirect_uri.as_str(),
843                ),
844                ("scope", &open_id_provider.open_id_config.scope),
845                ("claims", &claims),
846            ],
847        ) {
848            Ok(url) => url,
849            Err(e) => {
850                warn!(
851                    "failed to build authorization url for request {}: {}",
852                    self.request_id, e
853                );
854                self.show_error_page(
855                    503,
856                    "Authorization redirect failed",
857                    "Please contact your system administrator with the request id!",
858                    false,
859                );
860                return Action::Pause;
861            }
862        };
863
864        headers.push(("Location", location.as_str()));
865
866        self.send_http_response(307, headers, Some(b"Redirecting..."));
867
868        Action::Pause
869    }
870
871    /// Append the access token and id token to the request headers.
872    ///
873    /// ## Arguments
874    ///
875    /// * `auth_state` - The authorization state containing the access token and id token
876    fn append_headers(&self, auth_state: &AuthorizationState) {
877        // Forward access token in header, if configured
878        if let Some(header_name) = &self.plugin_config.access_token_header_name {
879            // Get access token
880            let access_token = &auth_state.access_token;
881            // Forward access token in header
882            self.add_http_request_header(
883                header_name,
884                format!(
885                    "{}{access_token}",
886                    self.plugin_config
887                        .access_token_header_prefix
888                        .as_ref()
889                        .unwrap(),
890                )
891                .as_str(),
892            );
893        }
894
895        // Forward id token in header, if configured
896        if let Some(header_name) = &self.plugin_config.id_token_header_name {
897            // Get id token
898            let id_token = &auth_state.id_token;
899            // Forward id token in header
900            self.add_http_request_header(
901                header_name,
902                format!(
903                    "{}{id_token}",
904                    self.plugin_config.id_token_header_prefix.as_ref().unwrap(),
905                )
906                .as_str(),
907            );
908        }
909    }
910
911    /// Get the cookie of the HTTP request by name
912    ///
913    /// ## Arguments
914    ///
915    /// * `name` - The name of the cookie to search for
916    ///
917    /// ## Returns
918    /// The value of the cookie if found, None otherwise
919    fn get_cookie(&self, name: &str) -> Option<String> {
920        let headers = self.get_http_request_headers();
921        for (key, value) in headers.iter() {
922            if key.to_lowercase().trim() == "cookie" {
923                let cookies: Vec<_> = value.split(';').collect();
924                for cookie_string in cookies {
925                    let Some(cookie_name_end) = cookie_string.find('=') else {
926                        continue;
927                    };
928                    let cookie_name = &cookie_string[0..cookie_name_end];
929                    if cookie_name.trim() == name {
930                        return Some(
931                            cookie_string[(cookie_name_end + 1)..cookie_string.len()].to_owned(),
932                        );
933                    }
934                }
935            }
936        }
937        None
938    }
939
940    /// Get the host of the HTTP request
941    ///
942    /// ## Returns
943    ///
944    /// The host is searched in the request headers. If the host is found, the value is returned.
945    fn get_host(&self) -> Option<String> {
946        self.get_http_request_header(":authority")
947            .or_else(|| self.get_http_request_header("host"))
948            .or_else(|| self.get_http_request_header("x-forwarded-host"))
949    }
950
951    /// Filter non proxy cookies by checking the cookie name.
952    /// This function removes all cookies from the request that do not match the cookie name to prevent
953    /// the cookie from being forwarded to the upstream service.
954    fn filter_proxy_cookies(&self) {
955        // Check if the filter_plugin_cookies option is set
956        if !self.plugin_config.filter_plugin_cookies {
957            return;
958        }
959
960        // Get all cookies
961        let all_cookies = self.get_http_request_header("cookie").unwrap_or_default();
962
963        // Remove non proxy cookies from request
964        let filtered_cookies = all_cookies
965            .split(';')
966            .filter(|x| !x.contains(&self.plugin_config.cookie_name))
967            .filter(|x| !x.contains(&format!("{}-nonce", self.plugin_config.cookie_name)))
968            .collect::<Vec<&str>>()
969            .join(";");
970
971        // Set the cookie header
972        self.set_http_request_header("Cookie", Some(&filtered_cookies));
973    }
974
975    /// Helper function to get the session cookie as a string by getting the cookie from the request
976    /// headers and concatenating all cookie parts.
977    ///
978    /// ## Returns
979    ///
980    /// The session cookie as a string if found, an error otherwise
981    pub fn get_session_cookie_as_string(&self) -> Result<String, PluginError> {
982        let cookie_name = &self.plugin_config.cookie_name;
983
984        // Get the number of cookie parts
985        let num_parts: u8 = self
986            .get_cookie(&format!("{cookie_name}-parts"))
987            .unwrap_or_default()
988            .parse()
989            .map_err(|_| PluginError::SessionCookieNotFoundError)?;
990
991        // Get the cookie parts and concatenate them into a string
992        let values = (0..num_parts)
993            .map(|i| self.get_cookie(&format!("{cookie_name}-{i}")))
994            .collect::<Option<Vec<String>>>()
995            .ok_or(PluginError::SessionCookieNotFoundError)?
996            .join("");
997
998        Ok(values)
999    }
1000
1001    // Get the encoded nonce from the cookie
1002    pub fn get_nonce(&self) -> Result<String, PluginError> {
1003        self.get_cookie(format!("{}-nonce", self.plugin_config.cookie_name).as_str())
1004            .ok_or(PluginError::NonceCookieNotFoundError)
1005    }
1006}