use std::{ env, num::NonZeroUsize, sync::{Arc, Mutex}, }; use lru::LruCache; use openidconnect::{ core::{CoreClient, CoreProviderMetadata, CoreResponseType}, reqwest::async_http_client, url::Url, AccessTokenHash, AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, OAuth2TokenResponse, PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, }; use uuid::Uuid; use serde::{Deserialize, Serialize}; pub struct LoginState { csrf_token: CsrfToken, nonce: Nonce, } pub struct Authenticator { pub client: CoreClient, pub login_cache: Arc>>, } #[derive(Eq, PartialEq, Hash, Debug, Clone, Serialize, Deserialize)] pub struct EndUserId(Uuid); impl EndUserId { pub fn new() -> Self { Self(Uuid::new_v4()) } } const LOGIN_CACHE_SIZE: usize = 50; pub const LOGIN_CALLBACK: &'static str = "/api/login_callback"; fn redirect_url(app_url: &str) -> RedirectUrl { RedirectUrl::new(format!("{}{}", app_url, LOGIN_CALLBACK)).unwrap() } impl Authenticator { pub async fn new( issuer_url: IssuerUrl, client_id: ClientId, client_secret: ClientSecret, redirect_uri: RedirectUrl, ) -> Self { // Use OpenID Connect Discovery to fetch the provider metadata. let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, async_http_client) .await .unwrap(); let client = CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)) // Set the URL the user will be redirected to after the authorization process. .set_redirect_uri(redirect_uri); Self { client, login_cache: Arc::new(Mutex::new(LruCache::new( NonZeroUsize::new(LOGIN_CACHE_SIZE).unwrap(), ))), } } pub async fn from_env() -> Self { let app_url = env::var("APP_URL").unwrap(); Authenticator::new( IssuerUrl::new(env::var("OPENID_ISSUER_URL").unwrap()).unwrap(), ClientId::new(env::var("OPENID_CLIENT_ID").unwrap()), ClientSecret::new(env::var("OPENID_CLIENT_SECRET").unwrap()), redirect_url(&app_url), ) .await } pub async fn get_login_url(&self) -> (EndUserId, Url) { let (auth_url, csrf_token, nonce) = self .client .authorize_url( AuthenticationFlow::::AuthorizationCode, CsrfToken::new_random, Nonce::new_random, ) .add_scope(Scope::new("email".to_string())) .add_scope(Scope::new("profile".to_string())) .url(); let user_id = EndUserId::new(); self.login_cache .lock().unwrap() .put(user_id.clone(), LoginState { csrf_token, nonce }); (user_id, auth_url) } }