use reqwest; use rsasl; use rsasl::callback::{Context, SessionData}; use rsasl::mechanisms::oauthbearer::properties::OAuthBearerError; use rsasl::validate::Validate; use miette::IntoDiagnostic; use crate::authentication::V; use crate::users::db::{User, UserData}; // Data required for validating access tokens pub struct OIDC { configuration_endpoint: String, userinfo_endpoint: String, userid_attribute: String, http: reqwest::blocking::Client, } impl OIDC { pub fn new(config: &crate::config::OIDCConfig) -> Self { let http = reqwest::blocking::Client::new(); let configuration_endpoint = config.configuration_endpoint.clone(); let userinfo_endpoint = config.userinfo_endpoint.clone(); let userid_attribute = config.userid_attribute.clone(); return Self { http, configuration_endpoint, userinfo_endpoint, userid_attribute, }; } pub fn user_by_token(&self, token: &str) -> miette::Result { let token_parts = token.split_whitespace().collect::>(); let only_token; if token_parts.len() == 1 { only_token = token_parts[0]; } else if token_parts.len() == 2 && token_parts[0].to_lowercase() == "bearer" { only_token = token_parts[1]; } else { return Err(miette::Error::msg("auth token has invalid format")); } let response: std::collections::HashMap = self .http .get(&self.userinfo_endpoint) .header("Authorization", &format!("Bearer {only_token}")) .send() .into_diagnostic()? .error_for_status() .into_diagnostic()? .json() .into_diagnostic()?; let id = response .get(&self.userid_attribute) .ok_or_else(|| { miette::Error::msg("configured userid_attribute missing from userinfo response") })? .as_str() .ok_or_else(|| miette::Error::msg("userid attribute value must be a string"))? .to_owned(); // TODO: check audience == client id - prevents logging in with a token for another service // TODO: get roles - from userdb or userinfo Ok(User { id, userdata: UserData::new(vec![]), }) } pub fn sasl_validate( &self, _session: &SessionData, context: &Context, validate: &mut Validate, ) { use rsasl::property as p; let maybe_token = context.get_ref::(); let token = match maybe_token { Some(token) => token, None => { return; } }; // TODO: dont do it twice (duplicate with callback) match self.user_by_token(token) { Ok(user) => { validate.finalize::(user); } Err(e) => { tracing::warn!("OIDC login failure: {}", e); } } } pub fn sasl_callback( &self, _session: &SessionData, context: &Context, request: &mut rsasl::callback::Request, ) -> Result<(), rsasl::prelude::SessionError> { request.satisfy_with::(| | { let maybe_token = context.get_ref::(); dbg!(&maybe_token); if let Some(token) = maybe_token { // TODO: dont do it twice (duplicate with validate) match self.user_by_token(token) { Ok(_user) => { return Ok(Ok(())); } Err(e) => { tracing::warn!("could not get user by token, {e}"); dbg!(e); } } } // FIXME: construct directly when #TODO is resolve let e_json = serde_json::json!({"status": "invalid_token", "openid-configuration": self.configuration_endpoint.to_owned()}); let x = serde_json::to_string(&e_json).unwrap(); // FIXME: leaks memory. make static or something. let x: &'static _ = Box::leak(x.into_boxed_str()); let e : OAuthBearerError = serde_json::from_str(&x).unwrap(); return Ok(Err(e)); })?; Ok(()) } }