mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
frontend: Janky code to make redirects after login work
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3116,6 +3116,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user