1use base64::{engine::general_purpose::STANDARD_NO_PAD as base64engine, Engine as _};
3
4use std::time::Duration;
6
7use jwt_simple::prelude::*;
9
10use log::{debug, warn};
12
13use std::sync::Arc;
15use std::vec;
16
17use proxy_wasm::traits::*;
19use proxy_wasm::types::*;
20
21use 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
32pub struct OidcHttpContext {
36 pub open_id_providers: Vec<OpenIdProvider>,
39 pub plugin_config: Arc<V2PluginConfiguration>,
41 pub token_id: Option<u32>,
43 pub request_id: String,
45}
46
47impl HttpContext for OidcHttpContext {
60 fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
62 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 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 self.request_should_be_excluded(&host, &path, &url) {
82 return Action::Continue;
83 }
84
85 let request_path = url.path();
88
89 if request_path == "/plugin-health" {
91 self.send_http_response(200, vec![], Some(b"OK"));
92 return Action::Pause;
93 }
94
95 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 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 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 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 match self.validate_cookie() {
153 Err(e) => match e {
154 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 self.append_headers(&auth_state);
178 self.filter_proxy_cookies();
180
181 return Action::Continue;
183 }
184 }
185
186 self.generate_auth_page();
189
190 Action::Pause
192 }
193}
194
195impl Context for OidcHttpContext {
197 fn on_http_call_response(&mut self, token_id: u32, _: usize, body_size: usize, _: usize) {
200 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 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
222impl OidcHttpContext {
224 fn request_should_be_excluded(&self, host: &str, path: &str, url: &Url) -> bool {
236 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 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 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 fn validate_cookie(&self) -> Result<AuthorizationState, PluginError> {
281 let cookie = self.get_session_cookie_as_string()?;
283 let nonce = self.get_nonce()?;
284
285 match Session::decode_and_decrypt(
287 cookie,
288 self.plugin_config.aes_key.reveal().clone(),
289 nonce,
290 ) {
291 Err(e) => Err(PluginError::CookieValidationError(e.to_string())),
293 Ok(session) => {
296 match self.plugin_config.token_validation {
298 true => {
299 let auth_state = match session.authorization_state {
301 Some(auth_state) => auth_state,
302 None => {
303 return Err(PluginError::AuthorizationStateNotFoundError);
304 }
305 };
306
307 match self.validate_token(&auth_state.id_token, &session.issuer) {
309 Ok(_) => {
311 debug!("token is valid, passing request");
312 Ok(auth_state)
313 }
314 Err(e) => Err(PluginError::TokenValidationError(e.into())),
316 }
317 }
318 false => match session.authorization_state {
319 Some(auth_state) => Ok(auth_state),
320 None => Err(PluginError::CookieValidationError(
322 "No authorization state found".to_string(),
323 )),
324 },
325 }
326 }
327 }
328 }
329
330 fn validate_token(&self, token: &str, issuer: &str) -> Result<(), PluginError> {
345 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 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 let verification_options = VerificationOptions {
367 allowed_issuers: Some(allowed_issuers),
368 allowed_audiences: Some(allowed_audiences),
369 ..Default::default()
370 };
371
372 for public_key in provider_to_use.public_keys.iter() {
374 let validation_result = public_key.verify_token(token, verification_options.clone());
376
377 match validation_result {
379 Ok(_) => return Ok(()),
380 Err(e) => {
381 debug!("token validation failed: {:?}", e);
382 continue;
383 }
384 }
385 }
386 Err(PluginError::NoKeyError)
388 }
389
390 fn provider_selection(&mut self, query: &str) -> Result<(), PluginError> {
397 let provider_selection_callback =
399 serde_urlencoded::from_str::<ProviderSelectionCallback>(query)?;
400
401 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 self.redirect_to_authorization_endpoint(
414 provider_to_authorize_with,
415 Some(provider_selection_callback.return_to),
416 );
417 Ok(())
418 }
419
420 fn exchange_code_for_token(&mut self, path: String) -> Result<(), PluginError> {
433 debug!("received request for OpenID callback");
434
435 let query = path.split('?').next_back().unwrap_or_default();
437 debug!("query: {query}");
438
439 let callback_params = serde_urlencoded::from_str::<CodeCallback>(query)?;
441
442 let encoded_cookie = self.get_session_cookie_as_string()?;
444 let encoded_nonce = self.get_nonce()?;
445
446 let session = Session::decode_and_decrypt(
448 encoded_cookie,
449 self.plugin_config.aes_key.reveal().clone(),
450 encoded_nonce,
451 )?;
452
453 let issuer = session.issuer.clone();
455
456 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 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 if state != session.state {
479 return Err(PluginError::StateMismatchError);
480 }
481
482 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 let code_verifier = session.code_verifier;
497
498 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 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 Err(_) => Err(PluginError::DispatchError),
532 Ok(id) => {
534 self.token_id = Some(id);
535 Ok(())
536 }
537 }
538 }
539
540 fn store_token_in_cookie(
552 &mut self,
553 token_id: u32,
554 body_size: usize,
555 ) -> Result<(), PluginError> {
556 if self.token_id != Some(token_id) {
558 return Err(PluginError::TokenIdMismatchError);
559 }
560
561 if self.get_http_call_response_header(":status") != Some("200".to_string()) {
563 match self.get_http_call_response_body(0, body_size) {
565 None => return Err(PluginError::NoBodyError),
567 Some(body) => {
568 match String::from_utf8(body) {
570 Ok(decoded) => return Err(PluginError::TokenResponseFormatError(decoded)),
571 Err(e) => return Err(PluginError::Utf8Error(e)),
573 }
574 }
575 }
576 }
577
578 match self.get_http_call_response_body(0, body_size) {
580 None => Err(PluginError::CookieStoreError(
582 "No body in response".to_string(),
583 )),
584 Some(body) => {
585 let encoded_cookie = self.get_session_cookie_as_string()?;
587 let encoded_nonce = self.get_nonce()?;
588
589 let mut session = Session::decode_and_decrypt(
591 encoded_cookie,
592 self.plugin_config.aes_key.reveal().clone(),
593 encoded_nonce,
594 )?;
595
596 let authorization_state = serde_json::from_slice::<AuthorizationState>(&body)?;
598 debug!("authorization state: {authorization_state:?}");
599
600 session.authorization_state = Some(authorization_state);
602
603 let (new_session, new_nonce) =
605 session.encrypt_and_encode(self.plugin_config.aes_key.reveal().clone())?;
606
607 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 let mut headers = Session::make_set_cookie_headers(&set_cookie_values);
617
618 let location_header = ("Location", session.original_path.as_str());
620 headers.push(location_header);
621
622 self.send_http_response(307, headers, Some(b"Redirecting..."));
624 Ok(())
625 }
626 }
627 }
628
629 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 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 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 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 fn generate_auth_page(&self) {
714 if self.open_id_providers.len() > 1 {
716 debug!("no session cookie found or invalid, showing auth page");
717
718 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 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 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 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 pub fn redirect_to_authorization_endpoint(
778 &self,
779 open_id_provider: &OpenIdProvider,
780 return_to: Option<String>,
781 ) -> Action {
782 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 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 let state_string = String::from_utf8_lossy(&pkce::code_verifier(128)).into_owned();
805
806 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 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 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 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 fn append_headers(&self, auth_state: &AuthorizationState) {
877 if let Some(header_name) = &self.plugin_config.access_token_header_name {
879 let access_token = &auth_state.access_token;
881 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 if let Some(header_name) = &self.plugin_config.id_token_header_name {
897 let id_token = &auth_state.id_token;
899 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 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 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 fn filter_proxy_cookies(&self) {
955 if !self.plugin_config.filter_plugin_cookies {
957 return;
958 }
959
960 let all_cookies = self.get_http_request_header("cookie").unwrap_or_default();
962
963 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 self.set_http_request_header("Cookie", Some(&filtered_cookies));
973 }
974
975 pub fn get_session_cookie_as_string(&self) -> Result<String, PluginError> {
982 let cookie_name = &self.plugin_config.cookie_name;
983
984 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 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 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}