From 2b7932dd49ff8b7db625d87f06eefdacc2d31060 Mon Sep 17 00:00:00 2001 From: yly Date: Thu, 14 Mar 2024 17:06:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=20Passkey=20=E7=99=BB?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 33 +++++++++++ Cargo.toml | 2 +- auth.conf | 7 ++- auth.db | Bin 32768 -> 32768 bytes loginpage.html | 4 +- register.html | 137 +++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 4 ++ src/entities/mod.rs | 4 +- src/main.rs | 18 +++--- src/services/auth.rs | 65 +++++++++++--------- src/services/mod.rs | 20 +++++-- 11 files changed, 248 insertions(+), 46 deletions(-) create mode 100644 register.html diff --git a/Cargo.lock b/Cargo.lock index 97f69ad..5233cae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -886,6 +886,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe" + [[package]] name = "httparse" version = "1.8.0" @@ -1080,6 +1086,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minijinja" version = "1.0.9" @@ -2214,10 +2230,18 @@ checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "bitflags 2.4.1", "bytes", + "futures-util", "http", "http-body", "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", + "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -2354,6 +2378,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/Cargo.toml b/Cargo.toml index 75236fe..254245d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ serde = "1.0.190" uuid = { version = "1.5.0", features = ["v4", "fast-rng"] } totp-rs = "5.4.0" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -tower-http = { version = "0.5", features = ["trace"] } +tower-http = { version = "0.5", features = ["trace", "fs"] } tracing = "0.1.40" askama = "0.10" minijinja = "1.0.9" diff --git a/auth.conf b/auth.conf index e9141de..f6b67c9 100644 --- a/auth.conf +++ b/auth.conf @@ -5,13 +5,14 @@ upstream protected { server { listen 8080; location / { - auth_request /auth; + auth_request /aaron/auth; set $original_full_url $scheme://$host$request_uri; error_page 401 =200 /login; proxy_set_header X-Original-URI $scheme://$host$request_uri; proxy_pass http://protected/; } - location = /auth { + location + location = /aaron/auth { internal; proxy_pass http://localhost:3000/auth; proxy_pass_request_body off; @@ -20,7 +21,7 @@ server { proxy_set_header X-Original-Remote-Addr $remote_addr; proxy_set_header X-Original-Host $host; } - location /login { + location /aaron/login { proxy_pass http://localhost:3000/login; proxy_set_header X-Original-Remote-Addr $remote_addr; proxy_set_header X-Original-Host $host; diff --git a/auth.db b/auth.db index 481218c98cc6a6315ce057b5dd391aafa79951cb..22b571f96f9553896b631974cc71afcbbd9cf505 100644 GIT binary patch literal 32768 zcmeI)O>g2x7zc1WvM-I?6}a-ghruQ)&z0xiYV-C2=gVPK*@yeWYUV75Gwl(q0%gulR=$kU8@=T<5>kM zolzDmRBQUvLhVp^q8$o5b1}rkM&f*ZV=W$!^XE>OWzbbU+(Z^L3(ICXv3^9Mp*?3Q z%%@Q)Jk>68SSVkN9<)se`xT+IU)d|`rG`+`s^vmS+ejqdTaAAj=Q+zhA`|~)z#h5` zb3~o++z1meq;R*swa0yPQ)8XIyBgQG|1QnB5j3Q^lhv(P;gcd)jjYD+-{%iQ?%7@X z`(t(?2p(lH+6%E3jqi~00Izz00bZa0SG_<0uX?}KM;6urOiiI&gV{p z=YKvr=GX@d1Rwwb2tWV=5P$##AOHaf{ObbWukp)2JZXL{wmotr?q2K-iyew)`cc(Q zRdoIHdO9yz9wn!}L!ysUB}pxIizDep)<2d{`@>yZxef}u!?Y#It;`|L7{IvW?)b0}$3Rh~Ps6(1q1iUr z5V8n{PuRg*>X@z@j_Z}C!N41=W0!>y^<&!!qB8-LQr3HzUl9Muq-Ge`CM>rhq2_$a zVtwOc62HspXJ^;>3%~~ZZu8NfIQGE;0SG_<0uX=z1Rwwb2tWV=HzV*}o4q5rgLedY zNAMrMBba@)gSq^JIsR8>`<|5P$##AOHafKmY;|fB*y_009Wx27%!B|E1V#F7`V1 v+ikcX+yew4009U<00Izz00bZa0SG|g76|M_n*4R90$lny(!6t>|Ns99s8Dil delta 47 zcmZo@U}|V!njp<6GEv5vRfIv$`s2ow1@=q~0-HDT{o

Login

-
+
+

Or:

+ Continue with Passkey
diff --git a/register.html b/register.html new file mode 100644 index 0000000..262eacf --- /dev/null +++ b/register.html @@ -0,0 +1,137 @@ + + + + + + + WebAuthn登录 + + + + + +

Welcome to the WebAuthn Server!

+ +
+ + + +
+ + + + \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 547c405..ea59be9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,7 +11,11 @@ pub const SESSION_ACTIVE_TIME: Lazy = Lazy::new(|| { pub const COOKIE_DOMAIN: Lazy = Lazy::new(|| env::var("DOMAIN").ok().unwrap_or(".aaronhu.cn".to_owned())); pub const LOGIN_PAGE_HTML: &str = include_str!("../loginpage.html"); +pub const REGISTER_PAGE_HTML: &str = include_str!("../register.html"); pub const PORT: Lazy = Lazy::new(|| env::var("PORT").unwrap_or("3000".to_string())); +// 部署此应用的URL,默认为/aaron +pub const HOME_URL: Lazy = + Lazy::new(|| env::var("HOME_URL").unwrap_or("/aaron".to_owned())); pub const DATABASE_URL: Lazy = Lazy::new(|| env::var("DATABASE_URL").unwrap_or("".to_owned())); pub const RP_ID: Lazy = diff --git a/src/entities/mod.rs b/src/entities/mod.rs index be38210..7a9dbb6 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -29,8 +29,8 @@ impl ServerState { // Now, with the builder you can define other options. // Set a "nice" relying party name. Has no security properties and // may be changed in the future. - let _RP_NAME = RP_NAME.to_string(); - let builder = builder.rp_name(&_RP_NAME); + let _rp_name = RP_NAME.to_string(); + let builder = builder.rp_name(&_rp_name); // Consume the builder and create our webauthn instance. let webauthn = Arc::new(builder.build().expect("Invalid configuration")); diff --git a/src/main.rs b/src/main.rs index b42a121..207a95e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,23 @@ -use axum::{routing::{get, post, Route}, Router}; +use axum::{routing::{get, get_service, post, Route}, Router}; use entities::*; -use services::{auth::{self, finish_authentication, start_authentication}, gc_task, login, login_page}; +use services::{auth::{self, finish_authentication, start_authentication}, gc_task, login, login_page, register_page}; use std::net::SocketAddr; use std::sync::Arc; -use tower_cookies::{CookieManagerLayer}; +use tower_cookies::CookieManagerLayer; use tower_http::trace::{self, TraceLayer}; use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer}; +use tower_http::services::{ServeDir, ServeFile}; use tracing::Level; pub mod config; pub mod controllers; pub mod entities; pub mod services; -use config::{PORT}; +use config::{HOME_URL, PORT}; #[tokio::main] async fn main() { // 初始化日志记录器 @@ -29,11 +30,11 @@ async fn main() { let app = Router::new() .route("/auth", get(crate::services::auth_otp)) // http://127.0.0.1:3000 .route("/login", get(login_page).post(login)) + .route("/register", get(register_page)) .route("/register_start/:username", post(auth::start_register)) - .route("/register_finish/:username", post(auth::finish_register)) + .route("/register_finish", post(auth::finish_register)) .route("/webauthn_login_start/:username", post(start_authentication)) - .route("/webauthn_login_finish/:username", post(finish_authentication)) - + .route("/webauthn_login_finish", post(finish_authentication)) .layer( TraceLayer::new_for_http() .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) @@ -42,7 +43,8 @@ async fn main() { .with_state(state.clone()) .layer(CookieManagerLayer::new()) .layer(session_layer); - let aaronhu = Router::new().nest("/aaron/", app); + // 嵌套防止撞路由 + let aaronhu = Router::new().nest(&HOME_URL, app); let port = PORT.parse::().unwrap_or(3000); let addr = SocketAddr::from(([127, 0, 0, 1], port)); // run it with hyper on localhost:3000 diff --git a/src/services/auth.rs b/src/services/auth.rs index 2e4646d..439efc8 100644 --- a/src/services/auth.rs +++ b/src/services/auth.rs @@ -11,7 +11,7 @@ use axum::{ response::IntoResponse, }; use std::time::Instant; -use tower_cookies::Cookies; +use tower_cookies::{Cookie, Cookies}; use tower_sessions::Session; use tracing::*; @@ -23,7 +23,7 @@ use tracing::*; // 1. Import the prelude - this contains everything needed for the server to function. use webauthn_rs::prelude::*; -use crate::config::{COOKIE_NAME, SESSION_ACTIVE_TIME}; +use crate::config::{COOKIE_DOMAIN, COOKIE_NAME, SESSION_ACTIVE_TIME}; use crate::ServerState; // 2. The first step a client (user) will carry out is requesting a credential to be @@ -61,7 +61,7 @@ use crate::ServerState; // the challenge to the browser. // TODO - Improve error handling and messages -fn auth_user(cookie_content:String,session_table:&mut HashMap) -> bool { +fn auth_user(cookie_content: String, session_table: &mut HashMap) -> bool { let Ok(uuid) = Uuid::parse_str(&cookie_content) else { return false; }; @@ -71,25 +71,29 @@ fn auth_user(cookie_content:String,session_table:&mut HashMap) -> if *expire <= Instant::now() { return false; } - session_table.insert(uuid, Instant::now()+Duration::from_secs(*SESSION_ACTIVE_TIME)); - tracing::info!("valid cookie {}",uuid); + session_table.insert( + uuid, + Instant::now() + Duration::from_secs(*SESSION_ACTIVE_TIME), + ); + tracing::info!("valid cookie {}", uuid); return true; - } -#[debug_handler] + pub async fn start_register( State(state): State>, cookies: Cookies, session: Session, Path(username): Path, ) -> Result, String> { - tracing::info!("Start register"); + tracing::info!("开始注册"); // todo!("Auth User"); let Some(cookie_content) = cookies.get(&COOKIE_NAME) else { + tracing::info!("用户没有Cookie"); return Err(WebauthnError::AuthenticationFailure.to_string()); }; let mut session_table = state.session.lock().await; - if !auth_user(cookie_content.value().to_string(), &mut session_table){ + if !auth_user(cookie_content.value().to_string(), &mut session_table) { + tracing::info!("用户Cookie无效,不在Session表中"); return Err(WebauthnError::AuthenticationFailure.to_string()); } // Remove any previous registrations that may have occurred from the session. @@ -166,7 +170,7 @@ pub async fn finish_register( session: Session, Json(reg): Json, ) -> Result { - info!("Confirming registration...."); + info!("完成注册..."); let pool = &state.db; let Ok(Some((user_name, user_id, reg_state))) = session .get::<(String, Uuid, PasskeyRegistration)>("reg_state") @@ -175,21 +179,20 @@ pub async fn finish_register( return Err(WebauthnError::AuthenticationFailure.to_string()); //Corrupt Session }; - let _ = session.remove::<(Uuid, PasskeyAuthentication)>("reg_state").await; + let _ = session + .remove::<(Uuid, PasskeyAuthentication)>("reg_state") + .await; let res = match state.webauthn.finish_passkey_registration(®, ®_state) { Ok(key) => { - info!("Passkey is okay"); + info!("Passkey 正常"); let uid = &user_id.to_string(); let username = &user_name.to_string(); // Check if the user_id already exists - let record = sqlx::query!( - "SELECT COUNT(KEY) AS count FROM users WHERE KEY = $1;", - uid - ) - .fetch_one(pool) - .await - .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; + let record = sqlx::query!("SELECT COUNT(KEY) AS count FROM users WHERE KEY = $1;", uid) + .fetch_one(pool) + .await + .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; // If the user doesn't exist, insert them into the users table if record.count == 0 @@ -329,6 +332,7 @@ pub async fn start_authentication( pub async fn finish_authentication( State(state): State>, session: Session, + cookies: Cookies, Json(auth): Json, ) -> Result { let pool = &state.db; @@ -383,13 +387,10 @@ pub async fn finish_authentication( } } - let user_name = sqlx::query!( - "SELECT NAME FROM users WHERE KEY = $1;", - uid - ) - .fetch_one(pool) - .await - .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; + let user_name = sqlx::query!("SELECT NAME FROM users WHERE KEY = $1;", uid) + .fetch_one(pool) + .await + .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; // Add our own values to the session session .insert("user_id", user_id) @@ -399,7 +400,17 @@ pub async fn finish_authentication( .insert("user_name", user_name.NAME) .await .map_err(|_| WebauthnError::InvalidUsername.to_string())?; - + let mut lock = state.session.lock().await; + let uuid = Uuid::new_v4(); + let expires = Instant::now() + Duration::from_secs(*SESSION_ACTIVE_TIME); + lock.insert( + uuid, + expires + ); + let mut new_cookie = Cookie::new(COOKIE_NAME.to_string(), uuid.to_string()); + new_cookie.set_domain(COOKIE_DOMAIN.to_string()); + cookies.add(new_cookie); + info!("从passkey登录创建了新Session{},过期时间{}s后",uuid,*SESSION_ACTIVE_TIME); StatusCode::OK } Err(e) => { diff --git a/src/services/mod.rs b/src/services/mod.rs index 4a1b0c2..5ab1896 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -16,6 +16,7 @@ use tower_cookies::{Cookie, Cookies}; use uuid::Uuid; +use crate::config::{HOME_URL, REGISTER_PAGE_HTML}; use crate::{ServerState, UserLoginForm}; use super::config::{COOKIE_DOMAIN, COOKIE_NAME, LOGIN_PAGE_HTML, SESSION_ACTIVE_TIME}; @@ -114,7 +115,7 @@ pub async fn login_page(headers: HeaderMap) -> impl IntoResponse { let uri = "?original_url=".to_owned() + uri; return Html( template - .render(context! { url => uri }) + .render(context! { url => uri, home_url => HOME_URL.to_string() }) .unwrap_or("Error".to_string()), ); } @@ -122,7 +123,18 @@ pub async fn login_page(headers: HeaderMap) -> impl IntoResponse { } Html( template - .render(context! { url => String::new() }) + .render(context! { url => String::new(), home_url => HOME_URL.to_string() }) + .unwrap_or("Error".to_string()), + ) +} +pub async fn register_page(headers: HeaderMap) -> impl IntoResponse { + tracing::info!("Headers: {:#?}", headers); + let mut env = Environment::new(); + env.add_template("register.html", REGISTER_PAGE_HTML).unwrap(); + let template = env.get_template("register.html").unwrap(); + Html( + template + .render(context! { url => String::new(), home_url => HOME_URL.to_string() }) .unwrap_or("Error".to_string()), ) } @@ -165,7 +177,7 @@ pub async fn login_with_passkey( let Ok(mut conn) = conn else { return Err((StatusCode::BAD_GATEWAY, "db连接错误")); }; - tracing::info!("{:?}", &frm); + tracing::info!("开始使用passkey登陆{:?}", &frm); let target = sqlx::query_as::<_, UserLoginForm>( r" SELECT NAME, KEY FROM USERS WHERE NAME = ? @@ -174,7 +186,7 @@ pub async fn login_with_passkey( .bind(frm.username) .fetch_optional(&mut *conn) .await; - tracing::info!("{:?}", &target); + tracing::info!("数据库返回 {:?}", &target); if let Ok(Some(target)) = target { if check_otp(target.otp, frm.otp) {