frontend: Janky code to make redirects after login work

This commit is contained in:
Lennart
2025-04-13 19:55:48 +02:00
parent 14e5533b6f
commit 64233f91d2
7 changed files with 89 additions and 21 deletions

1
Cargo.lock generated
View File

@@ -3116,6 +3116,7 @@ dependencies = [
"serde", "serde",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"url",
"uuid", "uuid",
] ]

View File

@@ -24,3 +24,4 @@ reqwest.workspace = true
rand.workspace = true rand.workspace = true
chrono.workspace = true chrono.workspace = true
uuid.workspace = true uuid.workspace = true
url.workspace = true

View File

@@ -4,6 +4,9 @@
<div class="login_window"> <div class="login_window">
<h1>Login</h1> <h1>Login</h1>
{% if let Some(redirect_uri) = redirect_uri %}
<p>and redirect to {{redirect_uri}}</p>
{% endif %}
<form action="login" method="post" id="form_login"> <form action="login" method="post" id="form_login">
<label for="username">Username</label> <label for="username">Username</label>
<input type="text" id="username" name="username" placeholder="username"> <input type="text" id="username" name="username" placeholder="username">
@@ -11,11 +14,19 @@
<label for="password">Password</label> <label for="password">Password</label>
<input type="password" id="password" name="password" placeholder="password"> <input type="password" id="password" name="password" placeholder="password">
<br> <br>
{% if let Some(redirect_uri) = redirect_uri %}
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
{% endif %}
<button type="submit">Login</button> <button type="submit">Login</button>
</form> </form>
{% if let Some(OidcProviderData {name, redirect_url}) = oidc_data %} {% if let Some(OidcProviderData {name, redirect_url}) = oidc_data %}
<a href="{{ redirect_url }}">Login with {{ name }}</a> <form action="{{ redirect_url }}" method="post" id="form_login">
{% if let Some(redirect_uri) = redirect_uri %}
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
{% endif %}
<button type="submit">Login with {{ name }}</button>
</form>
{% endif %} {% endif %}
</div> </div>

View File

@@ -12,7 +12,7 @@ use actix_web::{
use askama::Template; use askama::Template;
use askama_web::WebTemplate; use askama_web::WebTemplate;
use assets::{Assets, EmbedService}; use assets::{Assets, EmbedService};
use oidc::{route_get_oidc, route_get_oidc_callback}; use oidc::{route_get_oidc_callback, route_post_oidc};
use routes::{ use routes::{
addressbook::{route_addressbook, route_addressbook_restore}, addressbook::{route_addressbook, route_addressbook_restore},
calendar::{route_calendar, route_calendar_restore}, calendar::{route_calendar, route_calendar_restore},
@@ -44,7 +44,7 @@ struct UserPage {
async fn route_user(user: User, req: HttpRequest) -> Redirect { async fn route_user(user: User, req: HttpRequest) -> Redirect {
Redirect::to( Redirect::to(
req.url_for("frontend_user_named", &[&user.id]) req.url_for("frontend_user_named", &[user.id])
.unwrap() .unwrap()
.to_string(), .to_string(),
) )
@@ -105,15 +105,22 @@ async fn route_root(user: Option<User>, req: HttpRequest) -> impl Responder {
web::Redirect::to(redirect_url.to_string()).permanent() web::Redirect::to(redirect_url.to_string()).permanent()
} }
fn unauthorized_handler<B>(res: ServiceResponse<B>) -> actix_web::Result<ErrorHandlerResponse<B>> { pub(crate) fn unauthorized_handler<B>(
res: ServiceResponse<B>,
) -> actix_web::Result<ErrorHandlerResponse<B>> {
let (req, _) = res.into_parts(); 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!( let response = HttpResponse::Unauthorized().body(format!(
r#"<!Doctype html> r#"<!Doctype html>
<html> <html>
<head> <head>
<meta http-equiv="refresh" content="2; url={login_url}" /> <meta http-equiv="refresh" content="1; url={login_url}" />
</head> </head>
<body> <body>
Unauthorized, redirecting to <a href="{login_url}">login page</a> Unauthorized, redirecting to <a href="{login_url}">login page</a>
@@ -196,7 +203,7 @@ pub fn configure_frontend<AP: AuthenticationProvider, CS: CalendarStore, AS: Add
.service( .service(
web::resource("/login/oidc") web::resource("/login/oidc")
.name("frontend_login_oidc") .name("frontend_login_oidc")
.route(web::method(Method::GET).to(route_get_oidc)), .route(web::method(Method::POST).to(route_post_oidc)),
) )
.service( .service(
web::resource("/login/oidc/callback") web::resource("/login/oidc/callback")

View File

@@ -1,6 +1,10 @@
use actix_web::{ use actix_web::{
HttpRequest, HttpResponse, Responder, HttpRequest, HttpResponse, Responder,
http::header::{self}, http::{
StatusCode,
header::{self},
},
middleware::ErrorHandlers,
web::{self, Data, Form, Html, Json, Path, ServiceConfig}, web::{self, Data, Form, Html, Json, Path, ServiceConfig},
}; };
use askama::Template; use askama::Template;
@@ -11,6 +15,8 @@ use serde::{Deserialize, Serialize};
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::unauthorized_handler;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct NextcloudFlow { struct NextcloudFlow {
app_name: String, app_name: String,
@@ -217,6 +223,7 @@ pub fn configure_nextcloud_login<AP: AuthenticationProvider>(
) { ) {
cfg.service( cfg.service(
web::scope("/index.php/login/v2") web::scope("/index.php/login/v2")
.wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler))
.wrap(AuthenticationMiddleware::new(auth_provider.clone())) .wrap(AuthenticationMiddleware::new(auth_provider.clone()))
.app_data(Data::from(nextcloud_flows_state)) .app_data(Data::from(nextcloud_flows_state))
.app_data(Data::from(auth_provider.clone())) .app_data(Data::from(auth_provider.clone()))

View File

@@ -5,7 +5,7 @@ use actix_web::{
body::BoxBody, body::BoxBody,
error::UrlGenerationError, error::UrlGenerationError,
http::StatusCode, http::StatusCode,
web::{Data, Query, Redirect}, web::{Data, Form, Query, Redirect},
}; };
use openidconnect::{ use openidconnect::{
AuthenticationFlow, AuthorizationCode, ClaimsVerificationError, ConfigurationError, CsrfToken, AuthenticationFlow, AuthorizationCode, ClaimsVerificationError, ConfigurationError, CsrfToken,
@@ -67,6 +67,7 @@ struct OidcState {
state: CsrfToken, state: CsrfToken,
nonce: Nonce, nonce: Nonce,
pkce_verifier: PkceCodeVerifier, pkce_verifier: PkceCodeVerifier,
redirect_uri: Option<String>,
} }
fn get_http_client() -> reqwest::Client { fn get_http_client() -> reqwest::Client {
@@ -109,9 +110,15 @@ async fn get_oidc_client(
.set_redirect_uri(redirect_uri)) .set_redirect_uri(redirect_uri))
} }
#[derive(Debug, Deserialize)]
pub struct GetOidcForm {
redirect_uri: Option<String>,
}
/// Endpoint that redirects to the authorize endpoint of the OIDC service /// 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, req: HttpRequest,
Form(GetOidcForm { redirect_uri }): Form<GetOidcForm>,
config: Data<FrontendConfig>, config: Data<FrontendConfig>,
session: Session, session: Session,
) -> Result<impl Responder, OidcError> { ) -> Result<impl Responder, OidcError> {
@@ -146,6 +153,7 @@ pub async fn route_get_oidc(
state: csrf_token, state: csrf_token,
nonce, nonce,
pkce_verifier, pkce_verifier,
redirect_uri,
}, },
)?; )?;
@@ -225,16 +233,23 @@ pub async fn route_get_oidc_callback<AP: AuthenticationProvider>(
user = Some(new_user); 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 // Complete login flow
if let Some(user) = user { if let Some(user) = user {
session.insert("user", user.id.clone())?; session.insert("user", user.id.clone())?;
Ok( Ok(Redirect::to(redirect_uri)
Redirect::to(req.url_for_static("frontend_user")?.to_string()) .temporary()
.temporary() .respond_to(&req)
.respond_to(&req) .map_into_boxed_body())
.map_into_boxed_body(),
)
} else { } else {
// Add user provisioning // Add user provisioning
Ok(HttpResponse::build(StatusCode::UNAUTHORIZED).body("User does not exist")) Ok(HttpResponse::build(StatusCode::UNAUTHORIZED).body("User does not exist"))

View File

@@ -3,7 +3,7 @@ use actix_session::Session;
use actix_web::{ use actix_web::{
HttpRequest, HttpResponse, Responder, HttpRequest, HttpResponse, Responder,
error::ErrorUnauthorized, error::ErrorUnauthorized,
web::{Data, Form, Redirect}, web::{Data, Form, Query, Redirect},
}; };
use askama::Template; use askama::Template;
use askama_web::WebTemplate; use askama_web::WebTemplate;
@@ -13,11 +13,22 @@ use serde::Deserialize;
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "pages/login.html")] #[template(path = "pages/login.html")]
struct LoginPage<'a> { struct LoginPage<'a> {
redirect_uri: Option<String>,
oidc_data: Option<OidcProviderData<'a>>, oidc_data: Option<OidcProviderData<'a>>,
} }
pub async fn route_get_login(req: HttpRequest, config: Data<FrontendConfig>) -> impl Responder { #[derive(Debug, Deserialize)]
pub struct GetLoginQuery {
redirect_uri: Option<String>,
}
pub async fn route_get_login(
Query(GetLoginQuery { redirect_uri }): Query<GetLoginQuery>,
req: HttpRequest,
config: Data<FrontendConfig>,
) -> impl Responder {
LoginPage { LoginPage {
redirect_uri,
oidc_data: config.oidc.as_ref().map(|oidc| OidcProviderData { oidc_data: config.oidc.as_ref().map(|oidc| OidcProviderData {
name: &oidc.name, name: &oidc.name,
redirect_url: req redirect_url: req
@@ -33,20 +44,35 @@ pub async fn route_get_login(req: HttpRequest, config: Data<FrontendConfig>) ->
pub struct PostLoginForm { pub struct PostLoginForm {
username: String, username: String,
password: String, password: String,
redirect_uri: Option<String>,
} }
pub async fn route_post_login<AP: AuthenticationProvider>( pub async fn route_post_login<AP: AuthenticationProvider>(
req: HttpRequest, req: HttpRequest,
form: Form<PostLoginForm>, Form(PostLoginForm {
username,
password,
redirect_uri,
}): Form<PostLoginForm>,
session: Session, session: Session,
auth_provider: Data<AP>, auth_provider: Data<AP>,
) -> HttpResponse { ) -> 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 if let Ok(Some(user)) = auth_provider
.validate_user_token(&form.username, &form.password) .validate_user_token(&username, &password)
.await .await
{ {
session.insert("user", user.id).unwrap(); session.insert("user", user.id).unwrap();
Redirect::to(format!("/frontend/user/{}", &form.username)) Redirect::to(redirect_uri)
.see_other() .see_other()
.respond_to(&req) .respond_to(&req)
.map_into_boxed_body() .map_into_boxed_body()