use crate::http::security::session::{
CredentialAuthenticator, SessionAuthenticator, SessionConfig,
};
use crate::http::security::User;
use actix_web::http::header::LOCATION;
use actix_web::HttpResponse;
use serde::Deserialize;
#[derive(Clone)]
pub struct FormLoginConfig {
login_page: String,
login_processing_url: String,
username_parameter: String,
password_parameter: String,
default_success_url: String,
always_use_default_success_url: bool,
failure_url: String,
logout_url: String,
logout_success_url: String,
remember_me_parameter: Option<String>,
}
impl Default for FormLoginConfig {
fn default() -> Self {
Self::new()
}
}
impl FormLoginConfig {
pub fn new() -> Self {
Self {
login_page: "/login".to_string(),
login_processing_url: "/login".to_string(),
username_parameter: "username".to_string(),
password_parameter: "password".to_string(),
default_success_url: "/".to_string(),
always_use_default_success_url: false,
failure_url: "/login?error".to_string(),
logout_url: "/logout".to_string(),
logout_success_url: "/login?logout".to_string(),
remember_me_parameter: None,
}
}
pub fn login_page(mut self, url: &str) -> Self {
self.login_page = url.to_string();
self
}
pub fn login_processing_url(mut self, url: &str) -> Self {
self.login_processing_url = url.to_string();
self
}
pub fn username_parameter(mut self, param: &str) -> Self {
self.username_parameter = param.to_string();
self
}
pub fn password_parameter(mut self, param: &str) -> Self {
self.password_parameter = param.to_string();
self
}
pub fn default_success_url(mut self, url: &str) -> Self {
self.default_success_url = url.to_string();
self
}
pub fn always_use_default_success_url(mut self, always: bool) -> Self {
self.always_use_default_success_url = always;
self
}
pub fn failure_url(mut self, url: &str) -> Self {
self.failure_url = url.to_string();
self
}
pub fn logout_url(mut self, url: &str) -> Self {
self.logout_url = url.to_string();
self
}
pub fn logout_success_url(mut self, url: &str) -> Self {
self.logout_success_url = url.to_string();
self
}
pub fn remember_me_parameter(mut self, param: &str) -> Self {
self.remember_me_parameter = Some(param.to_string());
self
}
pub fn get_login_page(&self) -> &str {
&self.login_page
}
pub fn get_login_processing_url(&self) -> &str {
&self.login_processing_url
}
pub fn get_username_parameter(&self) -> &str {
&self.username_parameter
}
pub fn get_password_parameter(&self) -> &str {
&self.password_parameter
}
pub fn get_default_success_url(&self) -> &str {
&self.default_success_url
}
pub fn is_always_use_default_success_url(&self) -> bool {
self.always_use_default_success_url
}
pub fn get_failure_url(&self) -> &str {
&self.failure_url
}
pub fn get_logout_url(&self) -> &str {
&self.logout_url
}
pub fn get_logout_success_url(&self) -> &str {
&self.logout_success_url
}
pub fn get_remember_me_parameter(&self) -> Option<&str> {
self.remember_me_parameter.as_deref()
}
}
#[derive(Debug, Deserialize)]
pub struct LoginForm {
pub username: String,
pub password: String,
#[serde(default)]
pub remember_me: Option<String>,
}
impl LoginForm {
pub fn is_remember_me(&self) -> bool {
self.remember_me
.as_ref()
.map(|v| v == "on" || v == "true" || v == "1")
.unwrap_or(false)
}
}
#[derive(Clone)]
pub struct FormLoginHandler {
config: FormLoginConfig,
session_config: SessionConfig,
}
impl FormLoginHandler {
pub fn new(config: FormLoginConfig, session_config: SessionConfig) -> Self {
Self {
config,
session_config,
}
}
pub fn on_authentication_success(
&self,
session: &actix_session::Session,
user: &User,
saved_url: Option<String>,
) -> HttpResponse {
if SessionAuthenticator::login(session, user, &self.session_config).is_err() {
return self.on_authentication_failure();
}
let redirect_url = if self.config.always_use_default_success_url {
self.config.default_success_url.clone()
} else {
saved_url.unwrap_or_else(|| {
SessionAuthenticator::get_saved_request(
session,
&self.session_config,
&self.config.default_success_url,
)
})
};
HttpResponse::Found()
.insert_header((LOCATION, redirect_url))
.finish()
}
pub fn on_authentication_failure(&self) -> HttpResponse {
HttpResponse::Found()
.insert_header((LOCATION, self.config.failure_url.clone()))
.finish()
}
pub fn on_logout(&self, session: &actix_session::Session) -> HttpResponse {
SessionAuthenticator::logout(session, &self.session_config);
HttpResponse::Found()
.insert_header((LOCATION, self.config.logout_success_url.clone()))
.finish()
}
pub fn save_request(&self, session: &actix_session::Session, url: &str) {
let _ = SessionAuthenticator::save_request(session, url, &self.session_config);
}
pub fn login_page_url(&self) -> &str {
&self.config.login_page
}
pub fn login_processing_url(&self) -> &str {
&self.config.login_processing_url
}
pub fn logout_url(&self) -> &str {
&self.config.logout_url
}
pub fn is_login_page(&self, url: &str) -> bool {
url == self.config.login_page || url.starts_with(&format!("{}?", self.config.login_page))
}
pub fn is_login_processing_url(&self, url: &str) -> bool {
url == self.config.login_processing_url
}
pub fn is_logout_url(&self, url: &str) -> bool {
url == self.config.logout_url
}
pub fn config(&self) -> &FormLoginConfig {
&self.config
}
pub fn session_config(&self) -> &SessionConfig {
&self.session_config
}
}
#[derive(Clone)]
pub struct FormLoginService<A>
where
A: CredentialAuthenticator + Clone,
{
authenticator: A,
handler: FormLoginHandler,
}
impl<A> FormLoginService<A>
where
A: CredentialAuthenticator + Clone,
{
pub fn new(authenticator: A, config: FormLoginConfig, session_config: SessionConfig) -> Self {
Self {
authenticator,
handler: FormLoginHandler::new(config, session_config),
}
}
pub fn attempt_authentication(
&self,
session: &actix_session::Session,
username: &str,
password: &str,
) -> HttpResponse {
match self.authenticator.authenticate(username, password) {
Some(user) => self.handler.on_authentication_success(session, &user, None),
None => self.handler.on_authentication_failure(),
}
}
pub fn attempt_authentication_with_form(
&self,
session: &actix_session::Session,
form: &LoginForm,
) -> HttpResponse {
self.attempt_authentication(session, &form.username, &form.password)
}
pub fn logout(&self, session: &actix_session::Session) -> HttpResponse {
self.handler.on_logout(session)
}
pub fn handler(&self) -> &FormLoginHandler {
&self.handler
}
pub fn authenticator(&self) -> &A {
&self.authenticator
}
}
#[derive(Debug)]
pub enum FormLoginError {
InvalidCredentials,
SessionError(String),
MissingParameter(String),
}
impl std::fmt::Display for FormLoginError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FormLoginError::InvalidCredentials => write!(f, "Invalid credentials"),
FormLoginError::SessionError(e) => write!(f, "Session error: {}", e),
FormLoginError::MissingParameter(p) => write!(f, "Missing parameter: {}", p),
}
}
}
impl std::error::Error for FormLoginError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_form_login_config_default() {
let config = FormLoginConfig::new();
assert_eq!(config.get_login_page(), "/login");
assert_eq!(config.get_login_processing_url(), "/login");
assert_eq!(config.get_username_parameter(), "username");
assert_eq!(config.get_password_parameter(), "password");
assert_eq!(config.get_default_success_url(), "/");
assert_eq!(config.get_failure_url(), "/login?error");
assert_eq!(config.get_logout_url(), "/logout");
assert_eq!(config.get_logout_success_url(), "/login?logout");
}
#[test]
fn test_form_login_config_builder() {
let config = FormLoginConfig::new()
.login_page("/auth/login")
.login_processing_url("/auth/authenticate")
.username_parameter("user")
.password_parameter("pass")
.default_success_url("/dashboard")
.failure_url("/auth/login?failed=true")
.logout_url("/auth/logout")
.logout_success_url("/auth/login?loggedout")
.remember_me_parameter("rememberMe")
.always_use_default_success_url(true);
assert_eq!(config.get_login_page(), "/auth/login");
assert_eq!(config.get_login_processing_url(), "/auth/authenticate");
assert_eq!(config.get_username_parameter(), "user");
assert_eq!(config.get_password_parameter(), "pass");
assert_eq!(config.get_default_success_url(), "/dashboard");
assert!(config.is_always_use_default_success_url());
assert_eq!(config.get_failure_url(), "/auth/login?failed=true");
assert_eq!(config.get_logout_url(), "/auth/logout");
assert_eq!(config.get_logout_success_url(), "/auth/login?loggedout");
assert_eq!(config.get_remember_me_parameter(), Some("rememberMe"));
}
#[test]
fn test_login_form_remember_me() {
let form = LoginForm {
username: "user".to_string(),
password: "pass".to_string(),
remember_me: Some("on".to_string()),
};
assert!(form.is_remember_me());
let form2 = LoginForm {
username: "user".to_string(),
password: "pass".to_string(),
remember_me: Some("true".to_string()),
};
assert!(form2.is_remember_me());
let form3 = LoginForm {
username: "user".to_string(),
password: "pass".to_string(),
remember_me: None,
};
assert!(!form3.is_remember_me());
}
#[test]
fn test_form_login_handler_url_checks() {
let config = FormLoginConfig::new()
.login_page("/login")
.logout_url("/logout");
let handler = FormLoginHandler::new(config, SessionConfig::new());
assert!(handler.is_login_page("/login"));
assert!(handler.is_login_page("/login?error"));
assert!(!handler.is_login_page("/dashboard"));
assert!(handler.is_logout_url("/logout"));
assert!(!handler.is_logout_url("/login"));
}
}