diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 6893dc9..678fc45 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -429,6 +429,7 @@ dependencies = [ "openidconnect", "serde", "sqlx", + "utoipa", ] [[package]] diff --git a/backend/feature/auth/Cargo.toml b/backend/feature/auth/Cargo.toml index 27362a0..f820b30 100644 --- a/backend/feature/auth/Cargo.toml +++ b/backend/feature/auth/Cargo.toml @@ -11,3 +11,4 @@ log.workspace = true openidconnect.workspace = true serde.workspace = true sqlx.workspace = true +utoipa.workspace = true diff --git a/backend/feature/auth/src/adapter/delivery/oidc_callback_query_dto.rs b/backend/feature/auth/src/adapter/delivery/oidc_callback_query_dto.rs index 5cf4eb9..4bda4eb 100644 --- a/backend/feature/auth/src/adapter/delivery/oidc_callback_query_dto.rs +++ b/backend/feature/auth/src/adapter/delivery/oidc_callback_query_dto.rs @@ -1,6 +1,7 @@ use serde::Deserialize; +use utoipa::IntoParams; -#[derive(Deserialize)] +#[derive(Deserialize, IntoParams)] pub struct OidcCallbackQueryDto { pub code: String, pub state: String, diff --git a/backend/feature/auth/src/adapter/delivery/user_response_dto.rs b/backend/feature/auth/src/adapter/delivery/user_response_dto.rs index 2726c57..f040f2d 100644 --- a/backend/feature/auth/src/adapter/delivery/user_response_dto.rs +++ b/backend/feature/auth/src/adapter/delivery/user_response_dto.rs @@ -1,8 +1,9 @@ use serde::Serialize; +use utoipa::ToSchema; use crate::domain::entity::user::User; -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct UserResponseDto { pub id: i32, pub displayed_name: String, diff --git a/backend/feature/auth/src/framework/web.rs b/backend/feature/auth/src/framework/web.rs index 54b1487..a8945cb 100644 --- a/backend/feature/auth/src/framework/web.rs +++ b/backend/feature/auth/src/framework/web.rs @@ -1,4 +1,9 @@ +pub mod auth_api_doc; pub mod auth_middleware; pub mod auth_web_routes; +pub mod get_logged_in_user_handler; +pub mod oidc_callback_handler; +pub mod oidc_login_handler; +pub mod oidc_logout_handler; mod constants; diff --git a/backend/feature/auth/src/framework/web/auth_api_doc.rs b/backend/feature/auth/src/framework/web/auth_api_doc.rs new file mode 100644 index 0000000..4ab1dbe --- /dev/null +++ b/backend/feature/auth/src/framework/web/auth_api_doc.rs @@ -0,0 +1,17 @@ +use crate::framework::web::{ + get_logged_in_user_handler, oidc_callback_handler, oidc_login_handler, oidc_logout_handler, +}; +use utoipa::{OpenApi, openapi}; + +#[derive(OpenApi)] +#[openapi(paths( + get_logged_in_user_handler::get_logged_in_user_handler, + oidc_callback_handler::oidc_callback_handler, + oidc_login_handler::oidc_login_handler, + oidc_logout_handler::oidc_logout_handler +))] +struct ApiDoc; + +pub fn openapi() -> openapi::OpenApi { + ApiDoc::openapi() +} diff --git a/backend/feature/auth/src/framework/web/auth_web_routes.rs b/backend/feature/auth/src/framework/web/auth_web_routes.rs index d5dde49..5e2f9c8 100644 --- a/backend/feature/auth/src/framework/web/auth_web_routes.rs +++ b/backend/feature/auth/src/framework/web/auth_web_routes.rs @@ -1,15 +1,9 @@ -use actix_session::Session; -use actix_web::{HttpResponse, Responder, http::header, web}; +use actix_web::web; -use crate::{ - adapter::delivery::{ - auth_controller::AuthController, oidc_callback_query_dto::OidcCallbackQueryDto, - }, - application::error::auth_error::AuthError, - framework::web::{ - auth_middleware::UserId, - constants::{SESSION_KEY_AUTH_NONCE, SESSION_KEY_AUTH_STATE, SESSION_KEY_USER_ID}, - }, +use crate::framework::web::{ + get_logged_in_user_handler::get_logged_in_user_handler, + oidc_callback_handler::oidc_callback_handler, oidc_login_handler::oidc_login_handler, + oidc_logout_handler::oidc_logout_handler, }; pub fn configure_auth_routes(cfg: &mut web::ServiceConfig) { @@ -17,101 +11,8 @@ pub fn configure_auth_routes(cfg: &mut web::ServiceConfig) { web::scope("/auth") .route("/login", web::get().to(oidc_login_handler)) .route("/callback", web::get().to(oidc_callback_handler)) - .route("/logout", web::get().to(logout_handler)), + .route("/logout", web::get().to(oidc_logout_handler)), ); cfg.service(web::resource("/me").route(web::get().to(get_logged_in_user_handler))); } - -async fn oidc_login_handler( - auth_controller: web::Data, - session: Session, -) -> impl Responder { - let result = auth_controller.oidc_login(); - - match result { - Ok(auth_url) => { - if let Err(e) = session.insert::(SESSION_KEY_AUTH_STATE, auth_url.state) { - log::error!("{e:?}"); - return HttpResponse::InternalServerError().finish(); - } - if let Err(e) = session.insert::(SESSION_KEY_AUTH_NONCE, auth_url.nonce) { - log::error!("{e:?}"); - return HttpResponse::InternalServerError().finish(); - } - HttpResponse::Found() - .append_header((header::LOCATION, auth_url.url)) - .finish() - } - Err(e) => { - log::error!("{e:?}"); - HttpResponse::InternalServerError().finish() - } - } -} - -async fn oidc_callback_handler( - auth_controller: web::Data, - query: web::Query, - session: Session, -) -> impl Responder { - let expected_state = match session.get::(SESSION_KEY_AUTH_STATE) { - Ok(Some(state)) => state, - _ => return HttpResponse::BadRequest().finish(), - }; - - let expected_nonce = match session.get::(SESSION_KEY_AUTH_NONCE) { - Ok(Some(nonce)) => nonce, - _ => return HttpResponse::BadRequest().finish(), - }; - - let result = auth_controller - .oidc_callback(query.into_inner(), &expected_state, &expected_nonce) - .await; - - session.remove(SESSION_KEY_AUTH_STATE); - session.remove(SESSION_KEY_AUTH_NONCE); - match result { - Ok(user) => { - if let Err(e) = session.insert::(SESSION_KEY_USER_ID, user.id) { - log::error!("{e:?}"); - return HttpResponse::InternalServerError().finish(); - } - HttpResponse::Found() - .append_header((header::LOCATION, "/")) - .finish() - } - Err(e) => match e { - AuthError::InvalidAuthCode - | AuthError::InvalidIdToken - | AuthError::InvalidNonce - | AuthError::InvalidState => HttpResponse::BadRequest().finish(), - _ => { - log::error!("{e:?}"); - HttpResponse::InternalServerError().finish() - } - }, - } -} - -async fn logout_handler(session: Session) -> impl Responder { - session.clear(); - HttpResponse::Found() - .append_header((header::LOCATION, "/")) - .finish() -} - -async fn get_logged_in_user_handler( - auth_controller: web::Data, - user_id: UserId, -) -> impl Responder { - let result = auth_controller.get_user(user_id.get()).await; - - match result { - Ok(user) => HttpResponse::Ok().json(user), - Err(e) => { - log::error!("{e:?}"); - HttpResponse::InternalServerError().finish() - } - } -} diff --git a/backend/feature/auth/src/framework/web/get_logged_in_user_handler.rs b/backend/feature/auth/src/framework/web/get_logged_in_user_handler.rs new file mode 100644 index 0000000..a1415f8 --- /dev/null +++ b/backend/feature/auth/src/framework/web/get_logged_in_user_handler.rs @@ -0,0 +1,33 @@ +use actix_web::{HttpResponse, Responder, web}; + +use crate::{ + adapter::delivery::{auth_controller::AuthController, user_response_dto::UserResponseDto}, + framework::web::auth_middleware::UserId, +}; + +#[utoipa::path( + get, + path = "/me", + tag = "auth", + summary = "Get logged-in user information", + responses( + (status = 200, body = UserResponseDto), + ), + security( + ("oauth2" = []) + ) +)] +pub async fn get_logged_in_user_handler( + auth_controller: web::Data, + user_id: UserId, +) -> impl Responder { + let result = auth_controller.get_user(user_id.get()).await; + + match result { + Ok(user) => HttpResponse::Ok().json(user), + Err(e) => { + log::error!("{e:?}"); + HttpResponse::InternalServerError().finish() + } + } +} diff --git a/backend/feature/auth/src/framework/web/oidc_callback_handler.rs b/backend/feature/auth/src/framework/web/oidc_callback_handler.rs new file mode 100644 index 0000000..a6494c7 --- /dev/null +++ b/backend/feature/auth/src/framework/web/oidc_callback_handler.rs @@ -0,0 +1,69 @@ +use actix_session::Session; +use actix_web::{HttpResponse, Responder, http::header, web}; + +use crate::{ + adapter::delivery::{ + auth_controller::AuthController, oidc_callback_query_dto::OidcCallbackQueryDto, + }, + application::error::auth_error::AuthError, + framework::web::constants::{ + SESSION_KEY_AUTH_NONCE, SESSION_KEY_AUTH_STATE, SESSION_KEY_USER_ID, + }, +}; + +#[utoipa::path( + get, + path = "/auth/callback", + tag = "auth", + summary = "Handle OIDC callback", + params( + OidcCallbackQueryDto + ), + responses( + (status = 302, description = "Redirect to home page"), + (status = 400, description = "Invalid state or nonce"), + ) +)] +pub async fn oidc_callback_handler( + auth_controller: web::Data, + query: web::Query, + session: Session, +) -> impl Responder { + let expected_state = match session.get::(SESSION_KEY_AUTH_STATE) { + Ok(Some(state)) => state, + _ => return HttpResponse::BadRequest().finish(), + }; + + let expected_nonce = match session.get::(SESSION_KEY_AUTH_NONCE) { + Ok(Some(nonce)) => nonce, + _ => return HttpResponse::BadRequest().finish(), + }; + + let result = auth_controller + .oidc_callback(query.into_inner(), &expected_state, &expected_nonce) + .await; + + session.remove(SESSION_KEY_AUTH_STATE); + session.remove(SESSION_KEY_AUTH_NONCE); + match result { + Ok(user) => { + if let Err(e) = session.insert::(SESSION_KEY_USER_ID, user.id) { + log::error!("{e:?}"); + return HttpResponse::InternalServerError().finish(); + } + HttpResponse::Found() + .append_header((header::LOCATION, "/")) + .finish() + } + Err(e) => match e { + AuthError::InvalidAuthCode + | AuthError::InvalidIdToken + | AuthError::InvalidNonce + | AuthError::InvalidState => HttpResponse::BadRequest().finish(), + _ => { + log::error!("{e:?}"); + HttpResponse::InternalServerError().finish() + } + }, + } +} diff --git a/backend/feature/auth/src/framework/web/oidc_login_handler.rs b/backend/feature/auth/src/framework/web/oidc_login_handler.rs new file mode 100644 index 0000000..b804676 --- /dev/null +++ b/backend/feature/auth/src/framework/web/oidc_login_handler.rs @@ -0,0 +1,43 @@ +use actix_session::Session; +use actix_web::{HttpResponse, Responder, http::header, web}; + +use crate::{ + adapter::delivery::auth_controller::AuthController, + framework::web::constants::{SESSION_KEY_AUTH_NONCE, SESSION_KEY_AUTH_STATE}, +}; + +#[utoipa::path( + get, + path = "/auth/login", + tag = "auth", + summary = "Initiate OIDC login", + responses( + (status = 302, description = "Redirect to OIDC provider") + ) +)] +pub async fn oidc_login_handler( + auth_controller: web::Data, + session: Session, +) -> impl Responder { + let result = auth_controller.oidc_login(); + + match result { + Ok(auth_url) => { + if let Err(e) = session.insert::(SESSION_KEY_AUTH_STATE, auth_url.state) { + log::error!("{e:?}"); + return HttpResponse::InternalServerError().finish(); + } + if let Err(e) = session.insert::(SESSION_KEY_AUTH_NONCE, auth_url.nonce) { + log::error!("{e:?}"); + return HttpResponse::InternalServerError().finish(); + } + HttpResponse::Found() + .append_header((header::LOCATION, auth_url.url)) + .finish() + } + Err(e) => { + log::error!("{e:?}"); + HttpResponse::InternalServerError().finish() + } + } +} diff --git a/backend/feature/auth/src/framework/web/oidc_logout_handler.rs b/backend/feature/auth/src/framework/web/oidc_logout_handler.rs new file mode 100644 index 0000000..ea02cac --- /dev/null +++ b/backend/feature/auth/src/framework/web/oidc_logout_handler.rs @@ -0,0 +1,18 @@ +use actix_session::Session; +use actix_web::{HttpResponse, Responder, http::header}; + +#[utoipa::path( + get, + path = "/auth/logout", + tag = "auth", + summary = "Logout user", + responses( + (status = 302, description = "Redirect to home page") + ) +)] +pub async fn oidc_logout_handler(session: Session) -> impl Responder { + session.clear(); + HttpResponse::Found() + .append_header((header::LOCATION, "/")) + .finish() +} diff --git a/backend/server/src/api_doc.rs b/backend/server/src/api_doc.rs index a4ae5d9..e4d2e2a 100644 --- a/backend/server/src/api_doc.rs +++ b/backend/server/src/api_doc.rs @@ -1,4 +1,5 @@ use actix_web::web; +use auth::framework::web::auth_api_doc; use image::framework::web::image_api_doc; use post::framework::web::post_api_doc; use utoipa::{ @@ -37,8 +38,9 @@ impl utoipa::OpenApi for ApiDoc { pub fn configure_api_doc_routes(cfg: &mut web::ServiceConfig) { let openapi = ApiDoc::openapi() - .merge_from(post_api_doc::openapi()) - .merge_from(image_api_doc::openapi()); + .merge_from(auth_api_doc::openapi()) + .merge_from(image_api_doc::openapi()) + .merge_from(post_api_doc::openapi()); cfg.service(Redoc::with_url("/redoc", openapi)); }