Add authentication from upstream providers (#107)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Let BasicOIDC delegate authentication to upstream providers (Google, GitHub, GitLab, Keycloak...) Reviewed-on: #107
This commit is contained in:
@ -6,35 +6,46 @@ use actix_web::{web, HttpResponse, Responder};
|
||||
use askama::Template;
|
||||
|
||||
use crate::actors::users_actor;
|
||||
use crate::actors::users_actor::UsersActor;
|
||||
use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersActor};
|
||||
use crate::constants::TEMPORARY_PASSWORDS_LEN;
|
||||
use crate::controllers::settings_controller::BaseSettingsPage;
|
||||
use crate::data::action_logger::{Action, ActionLogger};
|
||||
use crate::data::app_config::AppConfig;
|
||||
use crate::data::client::{Client, ClientID, ClientManager};
|
||||
use crate::data::current_user::CurrentUser;
|
||||
use crate::data::provider::{Provider, ProviderID, ProvidersManager};
|
||||
use crate::data::user::{GeneralSettings, GrantedClients, User, UserID};
|
||||
use crate::utils::string_utils::rand_str;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "settings/clients_list.html")]
|
||||
struct ClientsListTemplate {
|
||||
_p: BaseSettingsPage,
|
||||
struct ClientsListTemplate<'a> {
|
||||
_p: BaseSettingsPage<'a>,
|
||||
clients: Vec<Client>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "settings/providers_list.html")]
|
||||
struct ProvidersListTemplate<'a> {
|
||||
_p: BaseSettingsPage<'a>,
|
||||
providers: Vec<Provider>,
|
||||
redirect_url: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "settings/users_list.html")]
|
||||
struct UsersListTemplate {
|
||||
_p: BaseSettingsPage,
|
||||
struct UsersListTemplate<'a> {
|
||||
_p: BaseSettingsPage<'a>,
|
||||
users: Vec<User>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "settings/edit_user.html")]
|
||||
struct EditUserTemplate {
|
||||
_p: BaseSettingsPage,
|
||||
struct EditUserTemplate<'a> {
|
||||
_p: BaseSettingsPage<'a>,
|
||||
u: User,
|
||||
clients: Vec<Client>,
|
||||
providers: Vec<Provider>,
|
||||
}
|
||||
|
||||
pub async fn clients_route(
|
||||
@ -51,6 +62,21 @@ pub async fn clients_route(
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn providers_route(
|
||||
user: CurrentUser,
|
||||
providers: web::Data<Arc<ProvidersManager>>,
|
||||
) -> impl Responder {
|
||||
HttpResponse::Ok().body(
|
||||
ProvidersListTemplate {
|
||||
_p: BaseSettingsPage::get("OpenID Providers list", &user, None, None),
|
||||
providers: providers.cloned(),
|
||||
redirect_url: AppConfig::get().oidc_provider_redirect_url(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct UpdateUserQuery {
|
||||
uid: UserID,
|
||||
@ -62,6 +88,8 @@ pub struct UpdateUserQuery {
|
||||
enabled: Option<String>,
|
||||
two_factor_exemption_after_successful_login: Option<String>,
|
||||
admin: Option<String>,
|
||||
allow_local_login: Option<String>,
|
||||
authorized_sources: String,
|
||||
grant_type: String,
|
||||
granted_clients: String,
|
||||
two_factor: String,
|
||||
@ -136,6 +164,29 @@ pub async fn users_route(
|
||||
}
|
||||
}
|
||||
|
||||
// Update the list of authorized authentication sources
|
||||
let auth_sources = AuthorizedAuthenticationSources {
|
||||
local: update.0.allow_local_login.is_some(),
|
||||
upstream: match update.0.authorized_sources.as_str() {
|
||||
"" => vec![],
|
||||
s => s.split(',').map(|s| ProviderID(s.to_string())).collect(),
|
||||
},
|
||||
};
|
||||
|
||||
if edited_user.authorized_authentication_sources() != auth_sources {
|
||||
logger.log(Action::AdminSetAuthorizedAuthenticationSources(
|
||||
&edited_user,
|
||||
&auth_sources,
|
||||
));
|
||||
users
|
||||
.send(users_actor::SetAuthorizedAuthenticationSources(
|
||||
edited_user.uid.clone(),
|
||||
auth_sources,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Update list of granted clients
|
||||
let granted_clients = match update.0.grant_type.as_str() {
|
||||
"all_clients" => GrantedClients::AllClients,
|
||||
@ -240,6 +291,7 @@ pub async fn users_route(
|
||||
pub async fn create_user(
|
||||
admin: CurrentUser,
|
||||
clients: web::Data<Arc<ClientManager>>,
|
||||
providers: web::Data<Arc<ProvidersManager>>,
|
||||
) -> impl Responder {
|
||||
let user = User {
|
||||
authorized_clients: Some(
|
||||
@ -257,6 +309,7 @@ pub async fn create_user(
|
||||
_p: BaseSettingsPage::get("Create a new user", admin.deref(), None, None),
|
||||
u: user,
|
||||
clients: clients.cloned(),
|
||||
providers: providers.cloned(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
@ -271,6 +324,7 @@ pub struct EditUserQuery {
|
||||
pub async fn edit_user(
|
||||
admin: CurrentUser,
|
||||
clients: web::Data<Arc<ClientManager>>,
|
||||
providers: web::Data<Arc<ProvidersManager>>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
query: web::Query<EditUserQuery>,
|
||||
) -> impl Responder {
|
||||
@ -293,6 +347,7 @@ pub async fn edit_user(
|
||||
),
|
||||
u: edited_account.unwrap_or_default(),
|
||||
clients: clients.cloned(),
|
||||
providers: providers.cloned(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
|
@ -2,6 +2,7 @@ use actix::Addr;
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Responder};
|
||||
use askama::Template;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::actors::bruteforce_actor::BruteForceActor;
|
||||
use crate::actors::users_actor::{LoginResult, UsersActor};
|
||||
@ -12,17 +13,18 @@ use crate::controllers::base_controller::{
|
||||
};
|
||||
use crate::data::action_logger::{Action, ActionLogger};
|
||||
use crate::data::login_redirect::LoginRedirect;
|
||||
use crate::data::provider::{Provider, ProvidersManager};
|
||||
use crate::data::remote_ip::RemoteIP;
|
||||
use crate::data::session_identity::{SessionIdentity, SessionStatus};
|
||||
use crate::data::user::User;
|
||||
use crate::data::webauthn_manager::WebAuthManagerReq;
|
||||
|
||||
struct BaseLoginPage<'a> {
|
||||
danger: Option<String>,
|
||||
success: Option<String>,
|
||||
page_title: &'static str,
|
||||
app_name: &'static str,
|
||||
redirect_uri: &'a LoginRedirect,
|
||||
pub struct BaseLoginPage<'a> {
|
||||
pub danger: Option<String>,
|
||||
pub success: Option<String>,
|
||||
pub page_title: &'static str,
|
||||
pub app_name: &'static str,
|
||||
pub redirect_uri: &'a LoginRedirect,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
@ -30,6 +32,7 @@ struct BaseLoginPage<'a> {
|
||||
struct LoginTemplate<'a> {
|
||||
_p: BaseLoginPage<'a>,
|
||||
login: String,
|
||||
providers: Vec<Provider>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
@ -77,6 +80,7 @@ pub struct LoginRequestQuery {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn login_route(
|
||||
remote_ip: RemoteIP,
|
||||
providers: web::Data<Arc<ProvidersManager>>,
|
||||
users: web::Data<Addr<UsersActor>>,
|
||||
bruteforce: web::Data<Addr<BruteForceActor>>,
|
||||
query: web::Query<LoginRequestQuery>,
|
||||
@ -121,7 +125,7 @@ pub async fn login_route(
|
||||
query.redirect.get_encoded()
|
||||
));
|
||||
}
|
||||
// Check if the user has to valide a second factor
|
||||
// Check if the user has to validate a second factor
|
||||
else if SessionIdentity(id.as_ref()).need_2fa_auth() {
|
||||
return redirect_user(&format!(
|
||||
"/2fa_auth?redirect={}",
|
||||
@ -132,7 +136,7 @@ pub async fn login_route(
|
||||
else if let Some(req) = &req {
|
||||
login = req.login.clone();
|
||||
let response: LoginResult = users
|
||||
.send(users_actor::LoginRequest {
|
||||
.send(users_actor::LocalLoginRequest {
|
||||
login: login.clone(),
|
||||
password: req.password.clone(),
|
||||
})
|
||||
@ -163,6 +167,12 @@ pub async fn login_route(
|
||||
danger = Some("Your account is disabled!".to_string());
|
||||
}
|
||||
|
||||
LoginResult::LocalAuthForbidden => {
|
||||
log::warn!("Failed login for username {} : attempted to use local auth, but it is forbidden", &login);
|
||||
logger.log(Action::TryLocalLoginFromUnauthorizedAccount(&login));
|
||||
danger = Some("You cannot login from local auth with your account!".to_string());
|
||||
}
|
||||
|
||||
LoginResult::Error => {
|
||||
danger = Some("An unkown error occured while trying to sign you in!".to_string());
|
||||
}
|
||||
@ -197,6 +207,7 @@ pub async fn login_route(
|
||||
redirect_uri: &query.redirect,
|
||||
},
|
||||
login,
|
||||
providers: providers.cloned(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
|
@ -5,6 +5,7 @@ pub mod base_controller;
|
||||
pub mod login_api;
|
||||
pub mod login_controller;
|
||||
pub mod openid_controller;
|
||||
pub mod providers_controller;
|
||||
pub mod settings_controller;
|
||||
pub mod two_factor_api;
|
||||
pub mod two_factors_controller;
|
||||
|
@ -20,8 +20,7 @@ use crate::data::code_challenge::CodeChallenge;
|
||||
use crate::data::current_user::CurrentUser;
|
||||
use crate::data::id_token::IdToken;
|
||||
use crate::data::jwt_signer::{JWTSigner, JsonWebKey};
|
||||
use crate::data::open_id_user_info::OpenIDUserInfo;
|
||||
use crate::data::openid_config::OpenIDConfig;
|
||||
use crate::data::openid_primitive::{OpenIDConfig, OpenIDUserInfo, TokenResponse};
|
||||
use crate::data::session_identity::SessionIdentity;
|
||||
use crate::data::user::User;
|
||||
use crate::utils::string_utils::rand_str;
|
||||
@ -255,16 +254,6 @@ pub struct TokenQuery {
|
||||
refresh_token_query: Option<TokenRefreshTokenQuery>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct TokenResponse {
|
||||
access_token: String,
|
||||
token_type: &'static str,
|
||||
refresh_token: String,
|
||||
expires_in: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
id_token: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn token(
|
||||
req: HttpRequest,
|
||||
query: web::Form<TokenQuery>,
|
||||
@ -451,9 +440,9 @@ pub async fn token(
|
||||
|
||||
TokenResponse {
|
||||
access_token: session.access_token.expect("Missing access token!"),
|
||||
token_type: "Bearer",
|
||||
refresh_token: session.refresh_token,
|
||||
expires_in: session.access_token_expire_at - time(),
|
||||
token_type: "Bearer".to_string(),
|
||||
refresh_token: Some(session.refresh_token),
|
||||
expires_in: Some(session.access_token_expire_at - time()),
|
||||
id_token: Some(jwt_signer.sign_token(id_token.to_jwt_claims())?),
|
||||
}
|
||||
}
|
||||
@ -501,9 +490,9 @@ pub async fn token(
|
||||
|
||||
TokenResponse {
|
||||
access_token: session.access_token.expect("Missing access token!"),
|
||||
token_type: "Bearer",
|
||||
refresh_token: session.refresh_token,
|
||||
expires_in: session.access_token_expire_at - time(),
|
||||
token_type: "Bearer".to_string(),
|
||||
refresh_token: Some(session.refresh_token),
|
||||
expires_in: Some(session.access_token_expire_at - time()),
|
||||
id_token: None,
|
||||
}
|
||||
}
|
||||
@ -637,12 +626,12 @@ async fn user_info(
|
||||
};
|
||||
|
||||
HttpResponse::Ok().json(OpenIDUserInfo {
|
||||
name: user.full_name(),
|
||||
name: Some(user.full_name()),
|
||||
sub: user.uid.0,
|
||||
given_name: user.first_name,
|
||||
family_name: user.last_name,
|
||||
preferred_username: user.username,
|
||||
email: user.email,
|
||||
email_verified: true,
|
||||
given_name: Some(user.first_name),
|
||||
family_name: Some(user.last_name),
|
||||
preferred_username: Some(user.username),
|
||||
email: Some(user.email),
|
||||
email_verified: Some(true),
|
||||
})
|
||||
}
|
||||
|
373
src/controllers/providers_controller.rs
Normal file
373
src/controllers/providers_controller.rs
Normal file
@ -0,0 +1,373 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix::Addr;
|
||||
use actix_identity::Identity;
|
||||
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::remote_ip::RemoteIP;
|
||||
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()))
|
||||
}
|
@ -12,31 +12,29 @@ use crate::data::current_user::CurrentUser;
|
||||
use crate::data::remote_ip::RemoteIP;
|
||||
use crate::data::user::User;
|
||||
|
||||
pub(crate) struct BaseSettingsPage {
|
||||
pub(crate) struct BaseSettingsPage<'a> {
|
||||
pub danger_message: Option<String>,
|
||||
pub success_message: Option<String>,
|
||||
pub page_title: &'static str,
|
||||
pub app_name: &'static str,
|
||||
pub is_admin: bool,
|
||||
pub user_name: String,
|
||||
pub user: &'a User,
|
||||
pub version: &'static str,
|
||||
pub ip_location_api: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl BaseSettingsPage {
|
||||
impl<'a> BaseSettingsPage<'a> {
|
||||
pub fn get(
|
||||
page_title: &'static str,
|
||||
user: &User,
|
||||
user: &'a User,
|
||||
danger_message: Option<String>,
|
||||
success_message: Option<String>,
|
||||
) -> BaseSettingsPage {
|
||||
) -> BaseSettingsPage<'a> {
|
||||
Self {
|
||||
danger_message,
|
||||
success_message,
|
||||
page_title,
|
||||
app_name: APP_NAME,
|
||||
is_admin: user.admin,
|
||||
user_name: user.username.to_string(),
|
||||
user,
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
ip_location_api: AppConfig::get().ip_location_service.as_deref(),
|
||||
}
|
||||
@ -45,15 +43,14 @@ impl BaseSettingsPage {
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "settings/account_details.html")]
|
||||
struct AccountDetailsPage {
|
||||
_p: BaseSettingsPage,
|
||||
u: User,
|
||||
struct AccountDetailsPage<'a> {
|
||||
_p: BaseSettingsPage<'a>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "settings/change_password.html")]
|
||||
struct ChangePasswordPage {
|
||||
_p: BaseSettingsPage,
|
||||
struct ChangePasswordPage<'a> {
|
||||
_p: BaseSettingsPage<'a>,
|
||||
min_pwd_len: usize,
|
||||
}
|
||||
|
||||
@ -63,7 +60,6 @@ pub async fn account_settings_details_route(user: CurrentUser) -> impl Responder
|
||||
HttpResponse::Ok().body(
|
||||
AccountDetailsPage {
|
||||
_p: BaseSettingsPage::get("Account details", &user, None, None),
|
||||
u: user,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
|
@ -17,14 +17,14 @@ use crate::data::webauthn_manager::WebAuthManagerReq;
|
||||
#[derive(Template)]
|
||||
#[template(path = "settings/two_factors_page.html")]
|
||||
struct TwoFactorsPage<'a> {
|
||||
_p: BaseSettingsPage,
|
||||
_p: BaseSettingsPage<'a>,
|
||||
user: &'a User,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "settings/add_2fa_totp_page.html")]
|
||||
struct AddTotpPage {
|
||||
_p: BaseSettingsPage,
|
||||
struct AddTotpPage<'a> {
|
||||
_p: BaseSettingsPage<'a>,
|
||||
qr_code: String,
|
||||
account_name: String,
|
||||
secret_key: String,
|
||||
@ -33,8 +33,8 @@ struct AddTotpPage {
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "settings/add_webauthn_page.html")]
|
||||
struct AddWebauhtnPage {
|
||||
_p: BaseSettingsPage,
|
||||
struct AddWebauhtnPage<'a> {
|
||||
_p: BaseSettingsPage<'a>,
|
||||
opaque_state: String,
|
||||
challenge_json: String,
|
||||
max_name_len: usize,
|
||||
|
Reference in New Issue
Block a user