All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			
		
			
				
	
	
		
			383 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			383 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use std::sync::Arc;
 | |
| 
 | |
| use crate::actors::bruteforce_actor::BruteForceActor;
 | |
| use crate::actors::providers_states_actor::{ProviderLoginState, ProvidersStatesActor};
 | |
| use crate::actors::users_actor::{LoginResult, UsersActor};
 | |
| use crate::actors::{bruteforce_actor, providers_states_actor, users_actor};
 | |
| use crate::constants::MAX_FAILED_LOGIN_ATTEMPTS;
 | |
| use crate::controllers::base_controller::{build_fatal_error_page, redirect_user};
 | |
| use crate::controllers::login_controller::BaseLoginPage;
 | |
| use crate::data::action_logger::{Action, ActionLogger};
 | |
| use crate::data::login_redirect::LoginRedirect;
 | |
| use crate::data::provider::{ProviderID, ProvidersManager};
 | |
| use crate::data::provider_configuration::ProviderConfigurationHelper;
 | |
| use crate::data::session_identity::{SessionIdentity, SessionStatus};
 | |
| use actix::Addr;
 | |
| use actix_identity::Identity;
 | |
| use actix_remote_ip::RemoteIP;
 | |
| use actix_web::{HttpRequest, HttpResponse, Responder, web};
 | |
| use askama::Template;
 | |
| 
 | |
| #[derive(askama::Template)]
 | |
| #[template(path = "login/prov_login_error.html")]
 | |
| struct ProviderLoginError<'a> {
 | |
|     p: BaseLoginPage,
 | |
|     message: &'a str,
 | |
| }
 | |
| 
 | |
| impl<'a> ProviderLoginError<'a> {
 | |
|     pub fn get(message: &'a str, redirect_uri: &'a LoginRedirect) -> HttpResponse {
 | |
|         let body = Self {
 | |
|             p: BaseLoginPage {
 | |
|                 page_title: "Upstream login",
 | |
|                 redirect_uri: redirect_uri.clone(),
 | |
|                 ..Default::default()
 | |
|             },
 | |
|             message,
 | |
|         }
 | |
|         .render()
 | |
|         .unwrap();
 | |
| 
 | |
|         HttpResponse::Unauthorized()
 | |
|             .content_type("text/html")
 | |
|             .body(body)
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(serde::Deserialize)]
 | |
| pub struct StartLoginQuery {
 | |
|     #[serde(default)]
 | |
|     redirect: LoginRedirect,
 | |
|     id: ProviderID,
 | |
| }
 | |
| 
 | |
| /// Start user authentication using a provider
 | |
| pub async fn start_login(
 | |
|     remote_ip: RemoteIP,
 | |
|     providers: web::Data<Arc<ProvidersManager>>,
 | |
|     states: web::Data<Addr<ProvidersStatesActor>>,
 | |
|     query: web::Query<StartLoginQuery>,
 | |
|     logger: ActionLogger,
 | |
|     id: Option<Identity>,
 | |
| ) -> impl Responder {
 | |
|     // Check if user is already authenticated
 | |
|     if SessionIdentity(id.as_ref()).is_authenticated() {
 | |
|         return redirect_user(query.redirect.get());
 | |
|     }
 | |
| 
 | |
|     // Get provider information
 | |
|     let provider = match providers.find_by_id(&query.id) {
 | |
|         None => {
 | |
|             return HttpResponse::NotFound()
 | |
|                 .body(build_fatal_error_page("Login provider not found!"));
 | |
|         }
 | |
|         Some(p) => p,
 | |
|     };
 | |
| 
 | |
|     // Generate & save state
 | |
|     let state = ProviderLoginState::new(&provider.id, query.redirect.clone());
 | |
|     states
 | |
|         .send(providers_states_actor::RecordState {
 | |
|             ip: remote_ip.0,
 | |
|             state: state.clone(),
 | |
|         })
 | |
|         .await
 | |
|         .unwrap();
 | |
| 
 | |
|     logger.log(Action::StartLoginAttemptWithOpenIDProvider {
 | |
|         provider_id: &provider.id,
 | |
|         state: &state.state_id,
 | |
|     });
 | |
| 
 | |
|     // Get provider configuration
 | |
|     let config = match ProviderConfigurationHelper::get_configuration(&provider).await {
 | |
|         Ok(c) => c,
 | |
|         Err(e) => {
 | |
|             log::error!("Failed to load provider configuration! {e}");
 | |
|             return HttpResponse::InternalServerError().body(build_fatal_error_page(
 | |
|                 "Failed to load provider configuration!",
 | |
|             ));
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     log::debug!("Provider configuration: {config:?}");
 | |
| 
 | |
|     let url = config.auth_url(&provider, &state);
 | |
|     log::debug!("Redirect user on {url} for authentication",);
 | |
| 
 | |
|     // Redirect user
 | |
|     redirect_user(&url)
 | |
| }
 | |
| 
 | |
| #[derive(serde::Deserialize)]
 | |
| pub struct FinishLoginSuccess {
 | |
|     code: String,
 | |
|     state: String,
 | |
| }
 | |
| 
 | |
| #[derive(serde::Deserialize)]
 | |
| pub struct FinishLoginError {
 | |
|     error: String,
 | |
|     error_description: Option<String>,
 | |
| }
 | |
| 
 | |
| #[derive(serde::Deserialize)]
 | |
| pub struct FinishLoginQuery {
 | |
|     #[serde(flatten)]
 | |
|     success: Option<FinishLoginSuccess>,
 | |
|     #[serde(flatten)]
 | |
|     error: Option<FinishLoginError>,
 | |
| }
 | |
| 
 | |
| /// Finish user authentication using a provider
 | |
| #[allow(clippy::too_many_arguments)]
 | |
| pub async fn finish_login(
 | |
|     remote_ip: RemoteIP,
 | |
|     providers: web::Data<Arc<ProvidersManager>>,
 | |
|     users: web::Data<Addr<UsersActor>>,
 | |
|     states: web::Data<Addr<ProvidersStatesActor>>,
 | |
|     bruteforce: web::Data<Addr<BruteForceActor>>,
 | |
|     query: web::Query<FinishLoginQuery>,
 | |
|     logger: ActionLogger,
 | |
|     id: Option<Identity>,
 | |
|     http_req: HttpRequest,
 | |
| ) -> impl Responder {
 | |
|     // Check if user is already authenticated
 | |
|     if SessionIdentity(id.as_ref()).is_authenticated() {
 | |
|         return redirect_user("/");
 | |
|     }
 | |
| 
 | |
|     let query = match query.0.success {
 | |
|         Some(q) => q,
 | |
|         None => {
 | |
|             let error_message = query
 | |
|                 .0
 | |
|                 .error
 | |
|                 .map(|e| e.error_description.unwrap_or(e.error))
 | |
|                 .unwrap_or("Authentication failed (unspecified error)!".to_string());
 | |
| 
 | |
|             logger.log(Action::ProviderError {
 | |
|                 message: error_message.as_str(),
 | |
|             });
 | |
| 
 | |
|             return ProviderLoginError::get(&error_message, &LoginRedirect::default());
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     // Get & consume state
 | |
|     let state = states
 | |
|         .send(providers_states_actor::ConsumeState {
 | |
|             ip: remote_ip.0,
 | |
|             state_id: query.state.clone(),
 | |
|         })
 | |
|         .await
 | |
|         .unwrap();
 | |
| 
 | |
|     let state = match state {
 | |
|         Some(s) => s,
 | |
|         None => {
 | |
|             logger.log(Action::ProviderCBInvalidState {
 | |
|                 state: query.state.as_str(),
 | |
|             });
 | |
|             log::warn!("User returned invalid state!");
 | |
|             return ProviderLoginError::get("Invalid state!", &LoginRedirect::default());
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     // We perform rate limiting before attempting to use authorization code
 | |
|     let failed_attempts = bruteforce
 | |
|         .send(bruteforce_actor::CountFailedAttempt {
 | |
|             ip: remote_ip.into(),
 | |
|         })
 | |
|         .await
 | |
|         .unwrap();
 | |
| 
 | |
|     if failed_attempts > MAX_FAILED_LOGIN_ATTEMPTS {
 | |
|         logger.log(Action::ProviderRateLimited);
 | |
|         return HttpResponse::TooManyRequests().body(build_fatal_error_page(
 | |
|             "Too many failed login attempts, please try again later!",
 | |
|         ));
 | |
|     }
 | |
| 
 | |
|     // Retrieve provider information & configuration
 | |
|     let provider = providers
 | |
|         .find_by_id(&state.provider_id)
 | |
|         .expect("Unable to retrieve provider information!");
 | |
| 
 | |
|     let provider_config = match ProviderConfigurationHelper::get_configuration(&provider).await {
 | |
|         Ok(c) => c,
 | |
|         Err(e) => {
 | |
|             log::error!("Failed to load provider configuration! {e}");
 | |
|             return HttpResponse::InternalServerError().body(build_fatal_error_page(
 | |
|                 "Failed to load provider configuration!",
 | |
|             ));
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     // Get access token & user information
 | |
|     let token = provider_config.get_token(&provider, &query.code).await;
 | |
|     let token = match token {
 | |
|         Ok(t) => t,
 | |
|         Err(e) => {
 | |
|             log::error!("Failed to retrieve login token! {e:?}");
 | |
| 
 | |
|             bruteforce
 | |
|                 .send(bruteforce_actor::RecordFailedAttempt {
 | |
|                     ip: remote_ip.into(),
 | |
|                 })
 | |
|                 .await
 | |
|                 .unwrap();
 | |
| 
 | |
|             logger.log(Action::ProviderFailedGetToken {
 | |
|                 state: &state,
 | |
|                 code: query.code.as_str(),
 | |
|             });
 | |
| 
 | |
|             return ProviderLoginError::get(
 | |
|                 "Failed to retrieve login token from identity provider!",
 | |
|                 &state.redirect,
 | |
|             );
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     // Use access token to get user information
 | |
|     let user_info = match provider_config.get_userinfo(&token).await {
 | |
|         Ok(info) => info,
 | |
|         Err(e) => {
 | |
|             log::error!("Failed to retrieve user information! {e:?}");
 | |
| 
 | |
|             logger.log(Action::ProviderFailedGetUserInfo {
 | |
|                 provider: &provider,
 | |
|             });
 | |
| 
 | |
|             return ProviderLoginError::get(
 | |
|                 "Failed to retrieve user information from identity provider!",
 | |
|                 &state.redirect,
 | |
|             );
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     // Check if user email is validated
 | |
|     if user_info.email_verified == Some(false) {
 | |
|         logger.log(Action::ProviderEmailNotValidated {
 | |
|             provider: &provider,
 | |
|         });
 | |
|         return ProviderLoginError::get(
 | |
|             &format!(
 | |
|                 "{} indicated that your email address has not been validated!",
 | |
|                 provider.name
 | |
|             ),
 | |
|             &state.redirect,
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     // Check if email was provided by the userinfo endpoint
 | |
|     let email = match &user_info.email {
 | |
|         Some(e) => e,
 | |
|         None => {
 | |
|             logger.log(Action::ProviderMissingEmailInResponse {
 | |
|                 provider: &provider,
 | |
|             });
 | |
|             return ProviderLoginError::get(
 | |
|                 &format!(
 | |
|                     "{} did not provide your email address in its reply, so we could not identify you!",
 | |
|                     provider.name
 | |
|                 ),
 | |
|                 &state.redirect,
 | |
|             );
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     // Get user from local database
 | |
|     let result: LoginResult = users
 | |
|         .send(users_actor::ProviderLoginRequest {
 | |
|             email: email.clone(),
 | |
|             user_info: user_info.clone(),
 | |
|             provider: provider.clone(),
 | |
|         })
 | |
|         .await
 | |
|         .unwrap();
 | |
| 
 | |
|     let user = match result {
 | |
|         LoginResult::Success(u) => u,
 | |
|         LoginResult::AccountAutoCreated(u) => {
 | |
|             logger.log(Action::ProviderAccountAutoCreated {
 | |
|                 provider: &provider,
 | |
|                 user: u.loggable(),
 | |
|             });
 | |
|             u
 | |
|         }
 | |
|         LoginResult::AccountNotFound => {
 | |
|             logger.log(Action::ProviderAccountNotFound {
 | |
|                 provider: &provider,
 | |
|                 email: email.as_str(),
 | |
|             });
 | |
| 
 | |
|             return ProviderLoginError::get(
 | |
|                 &format!("The email address {email} was not found in the database!"),
 | |
|                 &state.redirect,
 | |
|             );
 | |
|         }
 | |
|         LoginResult::AccountDisabled => {
 | |
|             logger.log(Action::ProviderAccountDisabled {
 | |
|                 provider: &provider,
 | |
|                 email: email.as_str(),
 | |
|             });
 | |
| 
 | |
|             return ProviderLoginError::get(
 | |
|                 &format!("The account associated with the email address {email} is disabled!"),
 | |
|                 &state.redirect,
 | |
|             );
 | |
|         }
 | |
| 
 | |
|         LoginResult::AuthFromProviderForbidden => {
 | |
|             logger.log(Action::ProviderAccountNotAllowedToLoginWithProvider {
 | |
|                 provider: &provider,
 | |
|                 email: email.as_str(),
 | |
|             });
 | |
| 
 | |
|             return ProviderLoginError::get(
 | |
|                 &format!(
 | |
|                     "The account associated with the email address {email} is not allowed to sign in using this provider!"
 | |
|                 ),
 | |
|                 &state.redirect,
 | |
|             );
 | |
|         }
 | |
| 
 | |
|         c => {
 | |
|             log::error!(
 | |
|                 "Login from provider {} failed with error {:?}",
 | |
|                 provider.id.0,
 | |
|                 c
 | |
|             );
 | |
| 
 | |
|             logger.log(Action::ProviderLoginFailed {
 | |
|                 provider: &provider,
 | |
|                 email: email.as_str(),
 | |
|             });
 | |
| 
 | |
|             return ProviderLoginError::get("Failed to complete login!", &state.redirect);
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     logger.log(Action::ProviderLoginSuccessful {
 | |
|         provider: &provider,
 | |
|         user: user.loggable(),
 | |
|     });
 | |
| 
 | |
|     let status = if user.has_two_factor() && !user.can_bypass_two_factors_for_ip(remote_ip.0) {
 | |
|         logger.log(Action::UserNeed2FAOnLogin {
 | |
|             user: user.loggable(),
 | |
|         });
 | |
|         SessionStatus::Need2FA
 | |
|     } else {
 | |
|         logger.log(Action::UserSuccessfullyAuthenticated {
 | |
|             user: user.loggable(),
 | |
|         });
 | |
|         SessionStatus::SignedIn
 | |
|     };
 | |
| 
 | |
|     SessionIdentity(id.as_ref()).set_user(&http_req, &user, status);
 | |
|     redirect_user(&format!("/login?redirect={}", state.redirect.get_encoded()))
 | |
| }
 |