feat(login): redirect to URL after login

This commit is contained in:
Matthieu Bessat 2024-11-11 20:57:04 +01:00
parent f990708052
commit 66b7a256cf
5 changed files with 35 additions and 7 deletions

1
Cargo.lock generated
View file

@ -1406,6 +1406,7 @@ dependencies = [
"redis", "redis",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded",
"sqlx", "sqlx",
"strum", "strum",
"strum_macros", "strum_macros",

View file

@ -58,6 +58,7 @@ rand = "0.8.5"
rand_core = { version = "0.6.4", features = ["std"] } rand_core = { version = "0.6.4", features = ["std"] }
url = "2.5.3" url = "2.5.3"
strum = "0.26.3" strum = "0.26.3"
serde_urlencoded = "0.7.1"
[build-dependencies] [build-dependencies]
minijinja-embed = "2.3.1" minijinja-embed = "2.3.1"

View file

@ -10,3 +10,11 @@
- [x] Get access token - [x] Get access token
- [ ] Support error responses by https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1 - [ ] Support error responses by https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
- [ ] Redirect to login when JWT expire
- [ ] Add admin panel via API
- [ ] Add admin CLI
- [ ] Support OpenID to use with demo client [oauth2c](https://github.com/cloudentity/oauth2c)
- .well-known/openid-configuration

View file

@ -1,7 +1,7 @@
use chrono::{Duration, SecondsFormat, Utc}; use chrono::{Duration, SecondsFormat, Utc};
use log::info; use log::info;
use serde::Deserialize; use serde::Deserialize;
use axum::{extract::State, http::{HeaderMap, HeaderValue, StatusCode}, response::{Html, IntoResponse}, Extension, Form}; use axum::{extract::{Query, State}, http::{HeaderMap, HeaderValue, StatusCode}, response::{Html, IntoResponse}, Extension, Form};
use fully_pub::fully_pub; use fully_pub::fully_pub;
use minijinja::context; use minijinja::context;
@ -28,9 +28,16 @@ struct LoginForm {
const DUMMY_PASSWORD_HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$+06ud2g4uVTI7kUIXjWM4g$6XqwuHt/+xl0d5J4BYKuIbg2acBp6udxMCnmJ6QfceY"; const DUMMY_PASSWORD_HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$+06ud2g4uVTI7kUIXjWM4g$6XqwuHt/+xl0d5J4BYKuIbg2acBp6udxMCnmJ6QfceY";
#[derive(Deserialize)]
#[fully_pub]
struct LoginQueryParams {
redirect_to: Option<String>
}
pub async fn perform_login( pub async fn perform_login(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Extension(renderer): Extension<TemplateRenderer>, Extension(renderer): Extension<TemplateRenderer>,
Query(query_params): Query<LoginQueryParams>,
Form(login): Form<LoginForm> Form(login): Form<LoginForm>
) -> impl IntoResponse { ) -> impl IntoResponse {
// get user from db // get user from db
@ -89,12 +96,15 @@ pub async fn perform_login(
let jwt_cookie = format!("minauth_jwt={jwt}; SameSite=Lax; Max-Age={cookie_max_age}"); let jwt_cookie = format!("minauth_jwt={jwt}; SameSite=Lax; Max-Age={cookie_max_age}");
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert("Set-Cookie", HeaderValue::from_str(&jwt_cookie).unwrap()); headers.insert("Set-Cookie", HeaderValue::from_str(&jwt_cookie).unwrap());
headers.insert("Location", HeaderValue::from_str(&format!("/me")).unwrap()); // TODO: check redirection for arbitrary URL, enforce relative path
headers.insert("Location", HeaderValue::from_str(
&query_params.redirect_to.unwrap_or(format!("/me"))
).unwrap());
( (
StatusCode::FOUND, StatusCode::SEE_OTHER,
headers, headers,
Html("") Html("Logged in. Redirecting you.")
).into_response() ).into_response()
} }

View file

@ -1,4 +1,6 @@
use axum::{extract::{Request, State}, http::StatusCode, middleware::Next, response::{Html, IntoResponse, Response}, Extension}; use std::collections::HashMap;
use axum::{extract::{OriginalUri, Query, Request, State}, http::{HeaderMap, HeaderValue, StatusCode}, middleware::Next, response::{Html, IntoResponse, Redirect, Response}, Extension};
use axum_extra::extract::CookieJar; use axum_extra::extract::CookieJar;
use crate::{ use crate::{
@ -36,6 +38,7 @@ pub async fn auth_middleware(
/// require auth /// require auth
pub async fn enforce_auth_middleware( pub async fn enforce_auth_middleware(
OriginalUri(original_uri): OriginalUri,
token_claims_ext: Option<Extension<UserTokenClaims>>, token_claims_ext: Option<Extension<UserTokenClaims>>,
req: Request, req: Request,
next: Next, next: Next,
@ -44,9 +47,14 @@ pub async fn enforce_auth_middleware(
Some(_val) => (), Some(_val) => (),
None => { None => {
// auth is required // auth is required
return Err( // redirect to login UI
(StatusCode::UNAUTHORIZED, Html("Unauthorized: auth is required on this page.")) let target_url = format!(
"/login?{}",
serde_urlencoded::to_string(&[
("redirect_to", original_uri.to_string())
]).expect("To encode URI")
); );
return Err(Redirect::to(&target_url));
} }
}; };
Ok(next.run(req).await) Ok(next.run(req).await)