use std::sync::Arc; use actix::Addr; use actix_identity::Identity; use actix_remote_ip::RemoteIP; use actix_web::{web, HttpRequest, HttpResponse, Responder}; use askama::Template; 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::{APP_NAME, 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}; #[derive(askama::Template)] #[template(path = "login/prov_login_error.html")] struct ProviderLoginError<'a> { _p: BaseLoginPage<'a>, message: &'a str, } impl<'a> ProviderLoginError<'a> { pub fn get(message: &'a str, redirect_uri: &'a LoginRedirect) -> HttpResponse { let body = Self { _p: BaseLoginPage { danger: None, success: None, page_title: "Upstream login", app_name: APP_NAME, redirect_uri, }, 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>, states: web::Data>, query: web::Query, logger: ActionLogger, id: Option, ) -> 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, } #[derive(serde::Deserialize)] pub struct FinishLoginQuery { #[serde(flatten)] success: Option, #[serde(flatten)] error: Option, } /// Finish user authentication using a provider #[allow(clippy::too_many_arguments)] pub async fn finish_login( remote_ip: RemoteIP, providers: web::Data>, users: web::Data>, states: web::Data>, bruteforce: web::Data>, query: web::Query, logger: ActionLogger, id: Option, 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(), provider: provider.clone(), }) .await .unwrap(); let user = match result { LoginResult::Success(u) => 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, }); let status = if user.has_two_factor() && !user.can_bypass_two_factors_for_ip(remote_ip.0) { logger.log(Action::UserNeed2FAOnLogin(&user)); SessionStatus::Need2FA } else { logger.log(Action::UserSuccessfullyAuthenticated(&user)); SessionStatus::SignedIn }; SessionIdentity(id.as_ref()).set_user(&http_req, &user, status); redirect_user(&format!("/login?redirect={}", state.redirect.get_encoded())) }