All checks were successful
continuous-integration/drone/push Build is passing
Use crates to reduce code base size : * `actix-remote-ip` to safely determine user IP location * `light-openid` for the OpenID primitives & as client to handle federation Reviewed-on: #111
374 lines
11 KiB
Rust
374 lines
11 KiB
Rust
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<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(),
|
|
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()))
|
|
}
|