diff --git a/Cargo.lock b/Cargo.lock index afbbd80..271ce91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1406,6 +1406,7 @@ dependencies = [ "redis", "serde", "serde_json", + "serde_urlencoded", "sqlx", "strum", "strum_macros", diff --git a/Cargo.toml b/Cargo.toml index 1bc5080..8f0903e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ rand = "0.8.5" rand_core = { version = "0.6.4", features = ["std"] } url = "2.5.3" strum = "0.26.3" +serde_urlencoded = "0.7.1" [build-dependencies] minijinja-embed = "2.3.1" diff --git a/TODO.md b/TODO.md index 9c01889..e97eac8 100644 --- a/TODO.md +++ b/TODO.md @@ -10,3 +10,11 @@ - [x] Get access token - [ ] 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 diff --git a/src/controllers/ui/login.rs b/src/controllers/ui/login.rs index 1178876..1a4b57f 100644 --- a/src/controllers/ui/login.rs +++ b/src/controllers/ui/login.rs @@ -1,7 +1,7 @@ use chrono::{Duration, SecondsFormat, Utc}; use log::info; 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 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"; +#[derive(Deserialize)] +#[fully_pub] +struct LoginQueryParams { + redirect_to: Option +} + pub async fn perform_login( State(app_state): State, Extension(renderer): Extension, + Query(query_params): Query, Form(login): Form ) -> impl IntoResponse { // 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 mut headers = HeaderMap::new(); 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, - Html("") + Html("Logged in. Redirecting you.") ).into_response() } diff --git a/src/middlewares/user_auth.rs b/src/middlewares/user_auth.rs index bc56894..1c11dc3 100644 --- a/src/middlewares/user_auth.rs +++ b/src/middlewares/user_auth.rs @@ -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 crate::{ @@ -36,6 +38,7 @@ pub async fn auth_middleware( /// require auth pub async fn enforce_auth_middleware( + OriginalUri(original_uri): OriginalUri, token_claims_ext: Option>, req: Request, next: Next, @@ -44,9 +47,14 @@ pub async fn enforce_auth_middleware( Some(_val) => (), None => { // auth is required - return Err( - (StatusCode::UNAUTHORIZED, Html("Unauthorized: auth is required on this page.")) + // redirect to login UI + 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)