Can complete authentication
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			This commit is contained in:
		
							
								
								
									
										105
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										105
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -1,5 +1,6 @@ | ||||
| use actix_web::{get, App, HttpResponse, HttpServer}; | ||||
| use actix_web::{get, web, App, HttpResponse, HttpServer}; | ||||
| use askama::Template; | ||||
| use actix_web::middleware::Logger; | ||||
|  | ||||
| use oidc_test_client::app_config::AppConfig; | ||||
| use oidc_test_client::openid_primitives::OpenIDConfig; | ||||
| @@ -26,6 +27,27 @@ struct HomeTemplate { | ||||
|     redirect_url: String, | ||||
| } | ||||
|  | ||||
| #[derive(Template)] | ||||
| #[template(path = "result.html")] | ||||
| struct ResultTemplate { | ||||
|     token: String, | ||||
|     user_info: String, | ||||
| } | ||||
|  | ||||
| #[derive(Template)] | ||||
| #[template(path = "error.html")] | ||||
| struct ErrorTemplate<'a> { | ||||
|     message: &'a str, | ||||
| } | ||||
|  | ||||
| impl<'a> ErrorTemplate<'a> { | ||||
|     pub fn build(message: &'a str) -> HttpResponse { | ||||
|         HttpResponse::Unauthorized() | ||||
|             .content_type("text/html") | ||||
|             .body(Self { message }.render().unwrap()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[get("/")] | ||||
| async fn home() -> HttpResponse { | ||||
|     HttpResponse::Ok().content_type("text/html").body( | ||||
| @@ -39,13 +61,25 @@ async fn home() -> HttpResponse { | ||||
|  | ||||
| #[get("/start")] | ||||
| async fn start(remote_ip: RemoteIP) -> HttpResponse { | ||||
|     let config = OpenIDConfig::load_from(&AppConfig::get().configuration_url) | ||||
|         .await | ||||
|         .expect("Failed to load provider configuration!"); | ||||
|     let config = match OpenIDConfig::load_from(&AppConfig::get().configuration_url).await { | ||||
|         Ok(c) => c, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to load OpenID configuration! {e}"); | ||||
|             return ErrorTemplate::build("Failed to load OpenID configuration!"); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     let state = match StateManager::gen_state(&remote_ip) { | ||||
|         Ok(s) => s, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to generate state! {:?}", e); | ||||
|             return ErrorTemplate::build("Failed to generate state!"); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     let authorization_url = config.authorization_url( | ||||
|         &AppConfig::get().client_id, | ||||
|         &StateManager::gen_state(&remote_ip).expect("Failed to generate state!"), | ||||
|         &state, | ||||
|         &AppConfig::get().redirect_url(), | ||||
|     ); | ||||
|  | ||||
| @@ -54,6 +88,65 @@ async fn start(remote_ip: RemoteIP) -> HttpResponse { | ||||
|         .finish() | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| struct RedirectQuery { | ||||
|     state: String, | ||||
|     code: String, | ||||
| } | ||||
|  | ||||
| #[get("/redirect")] | ||||
| async fn redirect(remote_ip: RemoteIP, query: web::Query<RedirectQuery>) -> HttpResponse { | ||||
|     // First, validate state | ||||
|     if let Err(e) = StateManager::validate_state(&remote_ip, &query.state) { | ||||
|         log::error!("Failed to validate state {}: {:?}", query.state, e); | ||||
|         return ErrorTemplate::build("State could not be validated!"); | ||||
|     } | ||||
|  | ||||
|     // Then, load OpenID configuration | ||||
|     let config = match OpenIDConfig::load_from(&AppConfig::get().configuration_url).await { | ||||
|         Ok(c) => c, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to load OpenID configuration! {e}"); | ||||
|             return ErrorTemplate::build("Failed to load OpenID configuration!"); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // Query token endpoint | ||||
|     let token = match config | ||||
|         .request_token( | ||||
|             &AppConfig::get().client_id, | ||||
|             &AppConfig::get().client_secret, | ||||
|             &query.code, | ||||
|             &AppConfig::get().redirect_url(), | ||||
|         ) | ||||
|         .await | ||||
|     { | ||||
|         Ok(t) => t, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to retrieve token! {}", e); | ||||
|             return ErrorTemplate::build("Failed to retrieve access token!"); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // Query userinfo endpoint | ||||
|     let user_info = match config.request_user_info(&token).await { | ||||
|         Ok(t) => t, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to retrieve user info! {}", e); | ||||
|             return ErrorTemplate::build("Failed to retrieve user info!"); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     HttpResponse::Ok().content_type("text/html").body( | ||||
|         ResultTemplate { | ||||
|             token: format!("{:#?}", token), | ||||
|             user_info: format!("{:#?}", user_info), | ||||
|         } | ||||
|         .render() | ||||
|         .unwrap(), | ||||
|     ) | ||||
| } | ||||
|  | ||||
| #[actix_web::main] | ||||
| async fn main() -> std::io::Result<()> { | ||||
|     env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); | ||||
| @@ -65,10 +158,12 @@ async fn main() -> std::io::Result<()> { | ||||
|  | ||||
|     HttpServer::new(|| { | ||||
|         App::new() | ||||
|             .wrap(Logger::default()) | ||||
|             .service(bootstrap) | ||||
|             .service(cover) | ||||
|             .service(home) | ||||
|             .service(start) | ||||
|             .service(redirect) | ||||
|     }) | ||||
|     .bind(&AppConfig::get().listen_addr) | ||||
|     .expect("Failed to bind server!") | ||||
|   | ||||
| @@ -1,3 +1,7 @@ | ||||
| use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; | ||||
| use base64::Engine; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use crate::Res; | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Deserialize)] | ||||
| @@ -7,6 +11,35 @@ pub struct OpenIDConfig { | ||||
|     pub userinfo_endpoint: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| pub struct TokenResponse { | ||||
|     pub access_token: String, | ||||
|     pub token_type: String, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub refresh_token: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub expires_in: Option<u64>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub id_token: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| pub struct UserInfo { | ||||
|     pub sub: String, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub name: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub given_name: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub family_name: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub preferred_username: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub email: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub email_verified: Option<bool>, | ||||
| } | ||||
|  | ||||
| impl OpenIDConfig { | ||||
|     /// Load OpenID configuration from a given URL | ||||
|     pub async fn load_from(url: &str) -> Res<Self> { | ||||
| @@ -21,4 +54,40 @@ impl OpenIDConfig { | ||||
|  | ||||
|         format!("{}?response_type=code&scope=openid%20profile%20email&client_id={client_id}&state={state}&redirect_uri={redirect_uri}", self.authorization_endpoint) | ||||
|     } | ||||
|  | ||||
|     /// Query the token endpoint | ||||
|     pub async fn request_token( | ||||
|         &self, | ||||
|         client_id: &str, | ||||
|         client_secret: &str, | ||||
|         code: &str, | ||||
|         redirect_uri: &str, | ||||
|     ) -> Res<TokenResponse> { | ||||
|         let authorization = BASE64_STANDARD.encode(format!("{}:{}", client_id, client_secret)); | ||||
|  | ||||
|         let mut params = HashMap::new(); | ||||
|         params.insert("grant_type", "authorization_code"); | ||||
|         params.insert("code", code); | ||||
|         params.insert("redirect_uri", redirect_uri); | ||||
|  | ||||
|         Ok(reqwest::Client::new() | ||||
|             .post(&self.token_endpoint) | ||||
|             .header("Authorization", format!("Basic {authorization}")) | ||||
|             .form(¶ms) | ||||
|             .send() | ||||
|             .await? | ||||
|             .json() | ||||
|             .await?) | ||||
|     } | ||||
|  | ||||
|     /// Query the UserInfo endpoint | ||||
|     pub async fn request_user_info(&self, token: &TokenResponse) -> Res<UserInfo> { | ||||
|         Ok(reqwest::Client::new() | ||||
|             .get(&self.userinfo_endpoint) | ||||
|             .header("Authorization", format!("Bearer {}", token.access_token)) | ||||
|             .send() | ||||
|             .await? | ||||
|             .json() | ||||
|             .await?) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| use std::error::Error; | ||||
| use std::fmt; | ||||
|  | ||||
| use crate::crypto_wrapper::CryptoWrapper; | ||||
| use crate::remote_ip::RemoteIP; | ||||
| use crate::time_utils::time; | ||||
| @@ -24,6 +27,20 @@ impl State { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Copy, Clone)] | ||||
| enum StateError { | ||||
|     InvalidIp, | ||||
|     Expired, | ||||
| } | ||||
|  | ||||
| impl Error for StateError {} | ||||
|  | ||||
| impl fmt::Display for StateError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||||
|         write!(f, "StateManager error {:?}", self) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl StateManager { | ||||
|     pub fn init() { | ||||
|         unsafe { | ||||
| @@ -37,4 +54,19 @@ impl StateManager { | ||||
|  | ||||
|         unsafe { WRAPPER.as_ref().unwrap() }.encrypt(&state) | ||||
|     } | ||||
|  | ||||
|     /// Validate generated state | ||||
|     pub fn validate_state(ip: &RemoteIP, state: &str) -> Res { | ||||
|         let state: State = unsafe { WRAPPER.as_ref().unwrap() }.decrypt(state)?; | ||||
|  | ||||
|         if state.ip != ip.0 { | ||||
|             return Err(Box::new(StateError::InvalidIp)); | ||||
|         } | ||||
|  | ||||
|         if state.expire < time() { | ||||
|             return Err(Box::new(StateError::Expired)); | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										9
									
								
								templates/error.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								templates/error.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| {% extends "base_page.html" %} | ||||
| {% block content %} | ||||
|  | ||||
| <div class="alert alert-danger" role="alert"> | ||||
|   {{ message }} | ||||
| </div> | ||||
|  | ||||
| <a class="btn btn-primary" href="/start" role="button">Start again</a> | ||||
| {% endblock content %} | ||||
							
								
								
									
										29
									
								
								templates/result.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								templates/result.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| {% extends "base_page.html" %} | ||||
| {% block content %} | ||||
|  | ||||
| <style> | ||||
|     .card { | ||||
|         text-align: left; | ||||
|     } | ||||
| </style> | ||||
|  | ||||
| <div class="alert alert-success" role="alert"> | ||||
|     Login successful | ||||
| </div> | ||||
|  | ||||
| <div class="card"> | ||||
|     <div class="card-body"> | ||||
|         <h5 class="card-title">Token response</h5> | ||||
|         <pre class="card-text">{{ token }}</pre> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div class="card"> | ||||
|     <div class="card-body"> | ||||
|         <h5 class="card-title">User info</h5> | ||||
|         <pre class="card-text">{{ user_info }}</pre> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <a class="btn btn-primary" href="/start" role="button">Start again</a> | ||||
| {% endblock content %} | ||||
		Reference in New Issue
	
	Block a user