From 64233f91d21491d0eaf8d2a0cbcc6450e8fb8741 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 13 Apr 2025 19:55:48 +0200 Subject: [PATCH] frontend: Janky code to make redirects after login work --- Cargo.lock | 1 + crates/frontend/Cargo.toml | 1 + .../public/templates/pages/login.html | 13 ++++++- crates/frontend/src/lib.rs | 19 ++++++---- crates/frontend/src/nextcloud_login.rs | 9 ++++- crates/frontend/src/oidc/mod.rs | 31 +++++++++++----- crates/frontend/src/routes/login.rs | 36 ++++++++++++++++--- 7 files changed, 89 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7b79fd..57d11cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3116,6 +3116,7 @@ dependencies = [ "serde", "thiserror 2.0.12", "tokio", + "url", "uuid", ] diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 37f580e..6bd9d84 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -24,3 +24,4 @@ reqwest.workspace = true rand.workspace = true chrono.workspace = true uuid.workspace = true +url.workspace = true diff --git a/crates/frontend/public/templates/pages/login.html b/crates/frontend/public/templates/pages/login.html index 044a71e..77ef407 100644 --- a/crates/frontend/public/templates/pages/login.html +++ b/crates/frontend/public/templates/pages/login.html @@ -4,6 +4,9 @@

Login

+ {% if let Some(redirect_uri) = redirect_uri %} +

and redirect to {{redirect_uri}}

+ {% endif %}
@@ -11,11 +14,19 @@
+ {% if let Some(redirect_uri) = redirect_uri %} + + {% endif %}
{% if let Some(OidcProviderData {name, redirect_url}) = oidc_data %} - Login with {{ name }} +
+ {% if let Some(redirect_uri) = redirect_uri %} + + {% endif %} + +
{% endif %}
diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index b72e6b5..d4fafb3 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -12,7 +12,7 @@ use actix_web::{ use askama::Template; use askama_web::WebTemplate; use assets::{Assets, EmbedService}; -use oidc::{route_get_oidc, route_get_oidc_callback}; +use oidc::{route_get_oidc_callback, route_post_oidc}; use routes::{ addressbook::{route_addressbook, route_addressbook_restore}, calendar::{route_calendar, route_calendar_restore}, @@ -44,7 +44,7 @@ struct UserPage { async fn route_user(user: User, req: HttpRequest) -> Redirect { Redirect::to( - req.url_for("frontend_user_named", &[&user.id]) + req.url_for("frontend_user_named", &[user.id]) .unwrap() .to_string(), ) @@ -105,15 +105,22 @@ async fn route_root(user: Option, req: HttpRequest) -> impl Responder { web::Redirect::to(redirect_url.to_string()).permanent() } -fn unauthorized_handler(res: ServiceResponse) -> actix_web::Result> { +pub(crate) fn unauthorized_handler( + res: ServiceResponse, +) -> actix_web::Result> { let (req, _) = res.into_parts(); - let login_url = req.url_for_static("frontend_login").unwrap().to_string(); + let redirect_uri = req.uri().to_string(); + let mut login_url = req.url_for_static("frontend_login").unwrap(); + login_url + .query_pairs_mut() + .append_pair("redirect_uri", &redirect_uri); + let login_url = login_url.to_string(); let response = HttpResponse::Unauthorized().body(format!( r#" - + Unauthorized, redirecting to login page @@ -196,7 +203,7 @@ pub fn configure_frontend( ) { cfg.service( web::scope("/index.php/login/v2") + .wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler)) .wrap(AuthenticationMiddleware::new(auth_provider.clone())) .app_data(Data::from(nextcloud_flows_state)) .app_data(Data::from(auth_provider.clone())) diff --git a/crates/frontend/src/oidc/mod.rs b/crates/frontend/src/oidc/mod.rs index a1014f8..1cfff7e 100644 --- a/crates/frontend/src/oidc/mod.rs +++ b/crates/frontend/src/oidc/mod.rs @@ -5,7 +5,7 @@ use actix_web::{ body::BoxBody, error::UrlGenerationError, http::StatusCode, - web::{Data, Query, Redirect}, + web::{Data, Form, Query, Redirect}, }; use openidconnect::{ AuthenticationFlow, AuthorizationCode, ClaimsVerificationError, ConfigurationError, CsrfToken, @@ -67,6 +67,7 @@ struct OidcState { state: CsrfToken, nonce: Nonce, pkce_verifier: PkceCodeVerifier, + redirect_uri: Option, } fn get_http_client() -> reqwest::Client { @@ -109,9 +110,15 @@ async fn get_oidc_client( .set_redirect_uri(redirect_uri)) } +#[derive(Debug, Deserialize)] +pub struct GetOidcForm { + redirect_uri: Option, +} + /// Endpoint that redirects to the authorize endpoint of the OIDC service -pub async fn route_get_oidc( +pub async fn route_post_oidc( req: HttpRequest, + Form(GetOidcForm { redirect_uri }): Form, config: Data, session: Session, ) -> Result { @@ -146,6 +153,7 @@ pub async fn route_get_oidc( state: csrf_token, nonce, pkce_verifier, + redirect_uri, }, )?; @@ -225,16 +233,23 @@ pub async fn route_get_oidc_callback( user = Some(new_user); } + let default_redirect = req.url_for_static("frontend_user")?.to_string(); + let redirect_uri = oidc_state.redirect_uri.unwrap_or(default_redirect.clone()); + let redirect_uri = req + .full_url() + .join(&redirect_uri) + .ok() + .and_then(|uri| req.full_url().make_relative(&uri)) + .unwrap_or(default_redirect); + // Complete login flow if let Some(user) = user { session.insert("user", user.id.clone())?; - Ok( - Redirect::to(req.url_for_static("frontend_user")?.to_string()) - .temporary() - .respond_to(&req) - .map_into_boxed_body(), - ) + Ok(Redirect::to(redirect_uri) + .temporary() + .respond_to(&req) + .map_into_boxed_body()) } else { // Add user provisioning Ok(HttpResponse::build(StatusCode::UNAUTHORIZED).body("User does not exist")) diff --git a/crates/frontend/src/routes/login.rs b/crates/frontend/src/routes/login.rs index 1442075..80d1dea 100644 --- a/crates/frontend/src/routes/login.rs +++ b/crates/frontend/src/routes/login.rs @@ -3,7 +3,7 @@ use actix_session::Session; use actix_web::{ HttpRequest, HttpResponse, Responder, error::ErrorUnauthorized, - web::{Data, Form, Redirect}, + web::{Data, Form, Query, Redirect}, }; use askama::Template; use askama_web::WebTemplate; @@ -13,11 +13,22 @@ use serde::Deserialize; #[derive(Template, WebTemplate)] #[template(path = "pages/login.html")] struct LoginPage<'a> { + redirect_uri: Option, oidc_data: Option>, } -pub async fn route_get_login(req: HttpRequest, config: Data) -> impl Responder { +#[derive(Debug, Deserialize)] +pub struct GetLoginQuery { + redirect_uri: Option, +} + +pub async fn route_get_login( + Query(GetLoginQuery { redirect_uri }): Query, + req: HttpRequest, + config: Data, +) -> impl Responder { LoginPage { + redirect_uri, oidc_data: config.oidc.as_ref().map(|oidc| OidcProviderData { name: &oidc.name, redirect_url: req @@ -33,20 +44,35 @@ pub async fn route_get_login(req: HttpRequest, config: Data) -> pub struct PostLoginForm { username: String, password: String, + redirect_uri: Option, } pub async fn route_post_login( req: HttpRequest, - form: Form, + Form(PostLoginForm { + username, + password, + redirect_uri, + }): Form, session: Session, auth_provider: Data, ) -> HttpResponse { + // Ensure that redirect_uri never goes cross-origin + let default_redirect = "/frontend/user".to_string(); + let redirect_uri = redirect_uri.unwrap_or(default_redirect.clone()); + let redirect_uri = req + .full_url() + .join(&redirect_uri) + .ok() + .and_then(|uri| req.full_url().make_relative(&uri)) + .unwrap_or(default_redirect); + if let Ok(Some(user)) = auth_provider - .validate_user_token(&form.username, &form.password) + .validate_user_token(&username, &password) .await { session.insert("user", user.id).unwrap(); - Redirect::to(format!("/frontend/user/{}", &form.username)) + Redirect::to(redirect_uri) .see_other() .respond_to(&req) .map_into_boxed_body()