diff --git a/src/web/api/v1/contexts/user/handlers.rs b/src/web/api/v1/contexts/user/handlers.rs index e167458e..4d10023b 100644 --- a/src/web/api/v1/contexts/user/handlers.rs +++ b/src/web/api/v1/contexts/user/handlers.rs @@ -59,7 +59,10 @@ pub async fn email_verification_handler(State(app_data): State>, Pa /// /// # Errors /// -/// It returns an error if the user could not be registered. +/// It returns an error if: +/// +/// - Unable to verify the supplied payload as a valid JWT. +/// - The JWT is not invalid or expired. #[allow(clippy::unused_async)] pub async fn login_handler( State(app_data): State>, @@ -96,6 +99,25 @@ pub async fn verify_token_handler( } } +/// It renews the JWT. +/// +/// # Errors +/// +/// It returns an error if: +/// +/// - Unable to parse the supplied payload as a valid JWT. +/// - The JWT is not invalid or expired. +#[allow(clippy::unused_async)] +pub async fn renew_token_handler( + State(app_data): State>, + extract::Json(token): extract::Json, +) -> Result>, ServiceError> { + match app_data.authentication_service.renew_token(&token.token).await { + Ok((token, user_compact)) => Ok(responses::renewed_token(token, user_compact)), + Err(error) => Err(error), + } +} + /// It returns the base API URL without the port. For example: `http://localhost`. fn api_base_url(host: &str) -> String { // HTTPS is not supported yet. diff --git a/src/web/api/v1/contexts/user/responses.rs b/src/web/api/v1/contexts/user/responses.rs index fa3d8359..731db068 100644 --- a/src/web/api/v1/contexts/user/responses.rs +++ b/src/web/api/v1/contexts/user/responses.rs @@ -27,7 +27,7 @@ pub struct TokenResponse { pub admin: bool, } -/// Response after successfully log in a user. +/// Response after successfully logging in a user. pub fn logged_in_user(token: String, user_compact: UserCompact) -> Json> { Json(OkResponse { data: TokenResponse { @@ -37,3 +37,14 @@ pub fn logged_in_user(token: String, user_compact: UserCompact) -> Json Json> { + Json(OkResponse { + data: TokenResponse { + token, + username: user_compact.username, + admin: user_compact.administrator, + }, + }) +} diff --git a/src/web/api/v1/contexts/user/routes.rs b/src/web/api/v1/contexts/user/routes.rs index 898dd710..22eefa0e 100644 --- a/src/web/api/v1/contexts/user/routes.rs +++ b/src/web/api/v1/contexts/user/routes.rs @@ -6,7 +6,9 @@ use std::sync::Arc; use axum::routing::{get, post}; use axum::Router; -use super::handlers::{email_verification_handler, login_handler, registration_handler, verify_token_handler}; +use super::handlers::{ + email_verification_handler, login_handler, registration_handler, renew_token_handler, verify_token_handler, +}; use crate::common::AppData; /// Routes for the [`user`](crate::web::api::v1::contexts::user) API context. @@ -24,5 +26,6 @@ pub fn router(app_data: Arc) -> Router { ) // Authentication .route("/login", post(login_handler).with_state(app_data.clone())) - .route("/token/verify", post(verify_token_handler).with_state(app_data)) + .route("/token/verify", post(verify_token_handler).with_state(app_data.clone())) + .route("/token/renew", post(renew_token_handler).with_state(app_data)) } diff --git a/tests/common/contexts/user/asserts.rs b/tests/common/contexts/user/asserts.rs index eb1a26cd..620096c3 100644 --- a/tests/common/contexts/user/asserts.rs +++ b/tests/common/contexts/user/asserts.rs @@ -1,6 +1,9 @@ use super::forms::RegistrationForm; +use super::responses::LoggedInUserData; use crate::common::asserts::assert_json_ok; -use crate::common::contexts::user::responses::{AddedUserResponse, SuccessfulLoginResponse, TokenVerifiedResponse}; +use crate::common::contexts::user::responses::{ + AddedUserResponse, SuccessfulLoginResponse, TokenRenewalData, TokenRenewalResponse, TokenVerifiedResponse, +}; use crate::common::responses::TextResponse; pub fn assert_added_user_response(response: &TextResponse) { @@ -28,3 +31,19 @@ pub fn assert_token_verified_response(response: &TextResponse) { assert_json_ok(response); } + +pub fn assert_token_renewal_response(response: &TextResponse, logged_in_user: &LoggedInUserData) { + let token_renewal_response: TokenRenewalResponse = serde_json::from_str(&response.body) + .unwrap_or_else(|_| panic!("response {:#?} should be a TokenRenewalResponse", response.body)); + + assert_eq!( + token_renewal_response.data, + TokenRenewalData { + token: logged_in_user.token.clone(), + username: logged_in_user.username.clone(), + admin: logged_in_user.admin, + } + ); + + assert_json_ok(response); +} diff --git a/tests/e2e/contexts/user/contract.rs b/tests/e2e/contexts/user/contract.rs index 7b381a4d..cb53d879 100644 --- a/tests/e2e/contexts/user/contract.rs +++ b/tests/e2e/contexts/user/contract.rs @@ -231,8 +231,10 @@ mod with_axum_implementation { use torrust_index_backend::web::api; use crate::common::client::Client; - use crate::common::contexts::user::asserts::{assert_successful_login_response, assert_token_verified_response}; - use crate::common::contexts::user::forms::{LoginForm, TokenVerificationForm}; + use crate::common::contexts::user::asserts::{ + assert_successful_login_response, assert_token_renewal_response, assert_token_verified_response, + }; + use crate::common::contexts::user::forms::{LoginForm, TokenRenewalForm, TokenVerificationForm}; use crate::e2e::config::ENV_VAR_E2E_EXCLUDE_AXUM_IMPL; use crate::e2e::contexts::user::steps::{new_logged_in_user, new_registered_user}; use crate::e2e::environment::TestEnv; @@ -265,6 +267,12 @@ mod with_axum_implementation { async fn it_should_allow_a_logged_in_user_to_verify_an_authentication_token() { let mut env = TestEnv::new(); env.start(api::Implementation::Axum).await; + + if env::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); let logged_in_user = new_logged_in_user(&env).await; @@ -277,5 +285,29 @@ mod with_axum_implementation { assert_token_verified_response(&response); } + + #[tokio::test] + async fn it_should_not_allow_a_logged_in_user_to_renew_an_authentication_token_which_is_still_valid_for_more_than_one_week( + ) { + let mut env = TestEnv::new(); + env.start(api::Implementation::Axum).await; + + if env::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + let logged_in_user = new_logged_in_user(&env).await; + + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_user.token); + + let response = client + .renew_token(TokenRenewalForm { + token: logged_in_user.token.clone(), + }) + .await; + + assert_token_renewal_response(&response, &logged_in_user); + } } }