Improve security
This commit is contained in:
@@ -33,7 +33,7 @@ pub async fn login(credentials: web::Json<Login>) -> Result<HttpResponse, AppErr
|
||||
match user.clone().verify(password)? {
|
||||
true => {
|
||||
log::info!("verified password successfully for user {}", user.id);
|
||||
let token: String = JwtToken::encode(user.clone().id);
|
||||
let token: String = JwtToken::encode(user.id, user.token_version);
|
||||
Ok(HttpResponse::Ok()
|
||||
.insert_header(("token", token))
|
||||
.insert_header(("user_id", user.id))
|
||||
|
||||
@@ -1,25 +1,62 @@
|
||||
use actix_web::HttpResponse;
|
||||
use diesel::prelude::*;
|
||||
|
||||
// JWT auth is stateless and there is no token blacklist, so logging out is
|
||||
// purely a client-side action (discarding the stored token). This endpoint
|
||||
// exists so the frontend has something to call and gets a clean response.
|
||||
pub async fn logout() -> HttpResponse {
|
||||
HttpResponse::Ok().finish()
|
||||
use crate::auth::extractor::AuthUser;
|
||||
use crate::database::establish_connection;
|
||||
use crate::error::AppError;
|
||||
use crate::schema::users;
|
||||
|
||||
/// Invalidates every token previously issued for this user by bumping
|
||||
/// `token_version` — the auth middleware rejects tokens whose `tv` claim no
|
||||
/// longer matches the stored value.
|
||||
pub async fn logout(auth_user: AuthUser) -> Result<HttpResponse, AppError> {
|
||||
let mut connection = establish_connection();
|
||||
|
||||
diesel::update(users::table.find(auth_user.0))
|
||||
.set(users::token_version.eq(users::token_version + 1))
|
||||
.execute(&mut connection)?;
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_service::Service;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{test, web, App};
|
||||
use actix_web::{test, web, App, HttpMessage};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use super::logout;
|
||||
use crate::auth::extractor::AuthUser;
|
||||
use crate::database::establish_connection;
|
||||
use crate::models::user::rss_user::User;
|
||||
use crate::schema::users;
|
||||
use crate::test_helpers::{delete_user, insert_user};
|
||||
|
||||
#[actix_web::test]
|
||||
async fn logout_returns_ok() {
|
||||
let app = test::init_service(App::new().route("/logout", web::post().to(logout))).await;
|
||||
async fn logout_bumps_token_version() {
|
||||
let mut connection = establish_connection();
|
||||
let user = insert_user(&mut connection, "secret");
|
||||
let user_id = user.id;
|
||||
let initial_version = user.token_version;
|
||||
|
||||
let app = test::init_service(
|
||||
App::new()
|
||||
.wrap_fn(move |req, srv| {
|
||||
req.extensions_mut().insert(AuthUser(user_id));
|
||||
srv.call(req)
|
||||
})
|
||||
.route("/logout", web::post().to(logout)),
|
||||
)
|
||||
.await;
|
||||
let req = test::TestRequest::post().uri("/logout").to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(StatusCode::OK, resp.status());
|
||||
|
||||
let updated: User = users::table.find(user_id).first(&mut connection).unwrap();
|
||||
assert_eq!(initial_version + 1, updated.token_version);
|
||||
|
||||
delete_user(&mut connection, user_id);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-3
@@ -1,3 +1,4 @@
|
||||
use actix_governor::{Governor, GovernorConfigBuilder};
|
||||
use actix_web::web;
|
||||
use actix_web::web::ServiceConfig;
|
||||
|
||||
@@ -12,9 +13,19 @@ pub fn auth_factory(app: &mut ServiceConfig) {
|
||||
backend: true,
|
||||
};
|
||||
|
||||
app.route(
|
||||
&base_path.define(String::from("/login")),
|
||||
web::post().to(login::login),
|
||||
// Login is the only unauthenticated endpoint that checks a password, so
|
||||
// it's the only one worth rate-limiting against brute-force/credential
|
||||
// stuffing. One request every 2s with a burst of 5 per IP.
|
||||
let login_rate_limit = GovernorConfigBuilder::default()
|
||||
.seconds_per_request(2)
|
||||
.burst_size(5)
|
||||
.finish()
|
||||
.expect("valid governor rate-limit config");
|
||||
|
||||
app.service(
|
||||
web::resource(base_path.define(String::from("/login")))
|
||||
.wrap(Governor::new(&login_rate_limit))
|
||||
.route(web::post().to(login::login)),
|
||||
);
|
||||
app.route(
|
||||
&base_path.define(String::from("/logout")),
|
||||
|
||||
Reference in New Issue
Block a user