use std::cell::RefCell; use std::collections::HashMap; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use base64::Engine as _; use crate::actors::providers_states_actor::ProviderLoginState; use crate::constants::OIDC_PROVIDERS_LIFETIME; use crate::data::app_config::AppConfig; use crate::data::jwt_signer::JsonWebKey; use crate::data::openid_primitive::{OpenIDUserInfo, TokenResponse}; use crate::data::provider::Provider; use crate::utils::err::Res; use crate::utils::time::time; #[derive(Debug, Clone, serde::Deserialize)] pub struct ProviderDiscovery { pub issuer: String, pub authorization_endpoint: String, pub token_endpoint: String, pub userinfo_endpoint: Option, pub jwks_uri: String, pub claims_supported: Option>, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ProviderJWKs { pub keys: Vec, } /// Provider configuration #[derive(Debug, Clone)] pub struct ProviderConfiguration { pub discovery: ProviderDiscovery, //pub keys: ProviderJWKs, pub expire: u64, } impl ProviderConfiguration { /// Get the URL where a user should be redirected to authenticate pub fn auth_url(&self, provider: &Provider, state: &ProviderLoginState) -> String { let authorization_url = &self.discovery.authorization_endpoint; let client_id = urlencoding::encode(&provider.client_id).to_string(); let state = urlencoding::encode(&state.state_id).to_string(); let callback_url = AppConfig::get().oidc_provider_redirect_url(); format!("{authorization_url}?response_type=code&scope=openid%20profile%20email&client_id={client_id}&state={state}&redirect_uri={callback_url}") } /// Retrieve the authorization token after a successful authentication, using an authorization code pub async fn get_token( &self, provider: &Provider, authorization_code: &str, ) -> Res { let authorization = BASE64_STANDARD.encode(format!("{}:{}", provider.client_id, provider.client_secret)); let redirect_url = AppConfig::get().oidc_provider_redirect_url(); let mut params = HashMap::new(); params.insert("grant_type", "authorization_code"); params.insert("code", authorization_code); params.insert("redirect_uri", redirect_url.as_str()); Ok(reqwest::Client::new() .post(&self.discovery.token_endpoint) .header("Authorization", format!("Basic {authorization}")) .form(¶ms) .send() .await? .json() .await?) } /// Retrieve information about the user, using given [TokenResponse] pub async fn get_userinfo(&self, token: &TokenResponse) -> Res { Ok(reqwest::Client::new() .get( self.discovery .userinfo_endpoint .as_ref() .expect("Userinfo endpoint is required by this implementation!"), ) .header("Authorization", format!("Bearer {}", token.access_token)) .send() .await? .json() .await?) } } thread_local! { static THREAD_CACHE: RefCell> = RefCell::new(Default::default()); } pub struct ProviderConfigurationHelper {} impl ProviderConfigurationHelper { /// Get or refresh the configuration for a provider pub async fn get_configuration(provider: &Provider) -> Res { let config = THREAD_CACHE.with(|i| i.borrow().get(&provider.configuration_url).cloned()); // Refresh config cache if needed if config.is_none() || config.as_ref().unwrap().expire < time() { let conf = Self::fetch_configuration(provider).await?; THREAD_CACHE.with(|i| { i.borrow_mut() .insert(provider.configuration_url.clone(), conf.clone()) }); return Ok(conf); } // We can return immediately previously extracted value Ok(config.unwrap()) } /// Get fresh configuration from provider async fn fetch_configuration(provider: &Provider) -> Res { let discovery: ProviderDiscovery = reqwest::get(&provider.configuration_url) .await? .json() .await?; // let keys: ProviderJWKs = reqwest::get(&discovery.jwks_uri).await?.json().await?; Ok(ProviderConfiguration { discovery, // keys, expire: time() + OIDC_PROVIDERS_LIFETIME, }) } }