Refactor; Fix CORS when login via passkeys
This commit is contained in:
parent
e494d71dc3
commit
3798e573b6
@ -110,10 +110,16 @@
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
if (response.ok){
|
||||
console.log("Logged In!");
|
||||
window.location.replace("/");
|
||||
var redir = await response.json();
|
||||
if(redir && redir.status === "ok"){
|
||||
console.log("Redirecting");
|
||||
window.location.replace(redir.redirect);
|
||||
} else {
|
||||
window.location.replace("/");
|
||||
}
|
||||
} else {
|
||||
console.log("Error");
|
||||
}
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
pub mod otp_controllers;
|
||||
pub mod passkey_login_controllers;
|
||||
pub mod passkey_register_controllers;
|
||||
@ -130,7 +130,7 @@ pub async fn finish_authentication(
|
||||
cookies.add(new_cookie);
|
||||
// 从Cookie中恢复重定向信息
|
||||
match original_uri {
|
||||
Some(redirect) => return Ok(Redirect::to(redirect.value())),
|
||||
Some(redirect) => return Ok(format!("{{\"status\": \"ok\", \"redirect\":\"{}\"}}",redirect.value().to_string())),
|
||||
_ => (),
|
||||
};
|
||||
// 处理完成重定向后,清除Cookie
|
||||
|
||||
169
src/controllers/passkey_register_controllers.rs
Normal file
169
src/controllers/passkey_register_controllers.rs
Normal file
@ -0,0 +1,169 @@
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::prelude::{CreationChallengeResponse, CredentialID, PasskeyAuthentication, PasskeyRegistration, RegisterPublicKeyCredential, WebauthnError};
|
||||
use crate::dto::credential_mapper::add_credential_by_id;
|
||||
|
||||
use crate::dto::user_mapper::{create_user_if_non_existent, get_user_count_by_uid};
|
||||
|
||||
use crate::SESSION_ACTIVE_TIME;
|
||||
|
||||
use axum::response::IntoResponse;
|
||||
|
||||
use crate::dto::{credential_mapper::get_credential_from_uid, user_mapper::get_uid_by_name};
|
||||
|
||||
|
||||
use crate::config::COOKIE_NAME;
|
||||
|
||||
use tracing::{info,debug,error};
|
||||
|
||||
use axum::extract::{Json, Path};
|
||||
|
||||
|
||||
use tower_sessions::Session;
|
||||
|
||||
use tower_cookies::Cookies;
|
||||
|
||||
use crate::entities::ServerState;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use axum::extract::State;
|
||||
|
||||
|
||||
// TODO - Improve error handling and messages
|
||||
fn auth_user(cookie_content: String, session_table: &mut HashMap<Uuid, Instant>) -> bool {
|
||||
let Ok(uuid) = Uuid::parse_str(&cookie_content) else {
|
||||
info!("此用户Session不在表中");
|
||||
return false;
|
||||
};
|
||||
let Some(expire) = session_table.get(&uuid) else {
|
||||
info!("此用户Session已过期");
|
||||
return false;
|
||||
};
|
||||
if *expire <= Instant::now() {
|
||||
info!("此用户Session已过期");
|
||||
return false;
|
||||
}
|
||||
session_table.insert(
|
||||
uuid,
|
||||
Instant::now() + Duration::from_secs(*SESSION_ACTIVE_TIME),
|
||||
);
|
||||
tracing::info!("valid cookie {}", uuid);
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn start_register(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
cookies: Cookies,
|
||||
session: Session,
|
||||
Path(username): Path<String>,
|
||||
) -> Result<Json<CreationChallengeResponse>, String> {
|
||||
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) {
|
||||
tracing::info!("用户Cookie无效,不在Session表中");
|
||||
return Err(WebauthnError::AuthenticationFailure.to_string());
|
||||
}
|
||||
// Remove any previous registrations that may have occurred from the session.
|
||||
let _ = session
|
||||
.remove::<(String, Uuid, PasskeyRegistration)>("reg_state")
|
||||
.await
|
||||
.map_err(|_| WebauthnError::AuthenticationFailure.to_string())?;
|
||||
let pool = &state.db;
|
||||
let username_tmp = username.clone();
|
||||
let (user_id, exclude_credentials): (Uuid, Option<Vec<CredentialID>>) =
|
||||
match get_uid_by_name(username_tmp, pool).await {
|
||||
Ok(uid) => {
|
||||
let exclude_passkeys = get_credential_from_uid(uid, pool).await?;
|
||||
(
|
||||
uid,
|
||||
Some(
|
||||
exclude_passkeys
|
||||
.into_iter()
|
||||
.map(|k| k.cred_id().clone())
|
||||
.collect(),
|
||||
),
|
||||
)
|
||||
}
|
||||
Err(_) => (Uuid::new_v4(), None),
|
||||
};
|
||||
|
||||
let res = match state.webauthn.start_passkey_registration(
|
||||
user_id,
|
||||
&username,
|
||||
&username,
|
||||
exclude_credentials,
|
||||
) {
|
||||
Ok((ccr, reg_state)) => {
|
||||
// Note that due to the session store in use being a server side memory store, this is
|
||||
// safe to store the reg_state into the session since it is not client controlled and
|
||||
// not open to replay attacks. If this was a cookie store, this would be UNSAFE.
|
||||
session
|
||||
.insert("reg_state", (username, user_id, reg_state))
|
||||
.await
|
||||
.map_err(|_| WebauthnError::ChallengePersistenceError.to_string())?;
|
||||
Json(ccr)
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("challenge_register -> {:?}", e);
|
||||
return Err(WebauthnError::ChallengePersistenceError.to_string());
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn finish_register(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
session: Session,
|
||||
Json(reg): Json<RegisterPublicKeyCredential>,
|
||||
) -> Result<impl IntoResponse, String> {
|
||||
info!("完成注册...");
|
||||
let pool = &state.db;
|
||||
let Ok(Some((user_name, user_id, reg_state))) = session
|
||||
.get::<(String, Uuid, PasskeyRegistration)>("reg_state")
|
||||
.await
|
||||
else {
|
||||
return Err(WebauthnError::AuthenticationFailure.to_string()); //Corrupt Session
|
||||
};
|
||||
|
||||
let _ = session
|
||||
.remove::<(Uuid, PasskeyAuthentication)>("reg_state")
|
||||
.await;
|
||||
|
||||
let res = match state.webauthn.finish_passkey_registration(®, ®_state) {
|
||||
Ok(key) => {
|
||||
info!("Passkey 正常");
|
||||
info!("检查用户是否存在");
|
||||
// Check if the user_id already exists
|
||||
let user_count = get_user_count_by_uid(user_id, pool).await?;
|
||||
// If the user doesn't exist, insert them into the users table
|
||||
if user_count == 0
|
||||
&& create_user_if_non_existent(user_id, user_name.to_string(), pool).await? != 1
|
||||
{
|
||||
return Err(WebauthnError::AuthenticationFailure.to_string());
|
||||
}
|
||||
|
||||
// Insert the key into the auth table
|
||||
if add_credential_by_id(user_id, &key, pool).await? != 1 {
|
||||
error!("将用户凭据持久化时失败,rows_affected!=1");
|
||||
return Err(WebauthnError::AuthenticationFailure.to_string());
|
||||
}
|
||||
|
||||
StatusCode::OK
|
||||
}
|
||||
Err(e) => {
|
||||
error!("challenge_register -> {:?}", e);
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
@ -6,7 +6,6 @@ use entities::*;
|
||||
|
||||
use controllers::passkey_login_controllers::{start_authentication, finish_authentication};
|
||||
use services::{
|
||||
auth::start_register,
|
||||
gc_services::gc_task,
|
||||
login, login_page, register_page,
|
||||
};
|
||||
@ -22,7 +21,8 @@ pub mod controllers;
|
||||
pub mod dto;
|
||||
pub mod entities;
|
||||
pub mod services;
|
||||
use crate::{config::*, services::auth::finish_register};
|
||||
use config::*;
|
||||
use controllers::passkey_register_controllers::{finish_register,start_register};
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// 初始化日志记录器
|
||||
|
||||
@ -1,231 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum::{
|
||||
extract::{Json, Path},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use std::time::Instant;
|
||||
use tower_cookies::Cookies;
|
||||
use tower_sessions::Session;
|
||||
use tracing::*;
|
||||
|
||||
/*
|
||||
* Webauthn RS auth handlers.
|
||||
* These files use webauthn to process the data received from each route, and are closely tied to axum
|
||||
*/
|
||||
|
||||
// 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::dto::credential_mapper::{add_credential_by_id, get_credential_from_uid};
|
||||
use crate::dto::user_mapper::{
|
||||
create_user_if_non_existent, get_uid_by_name, get_user_count_by_uid,
|
||||
};
|
||||
use crate::ServerState;
|
||||
|
||||
// 2. The first step a client (user) will carry out is requesting a credential to be
|
||||
// registered. We need to provide a challenge for this. The work flow will be:
|
||||
//
|
||||
// ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
// │ Authenticator │ │ Browser │ │ Site │
|
||||
// └───────────────┘ └───────────────┘ └───────────────┘
|
||||
// │ │ │
|
||||
// │ │ 1. Start Reg │
|
||||
// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│
|
||||
// │ │ │
|
||||
// │ │ 2. Challenge │
|
||||
// │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
|
||||
// │ │ │
|
||||
// │ 3. Select Token │ │
|
||||
// ─ ─ ─│◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │
|
||||
// 4. Verify │ │ │ │
|
||||
// │ 4. Yield PubKey │ │
|
||||
// └ ─ ─▶│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ │
|
||||
// │ │ │
|
||||
// │ │ 5. Send Reg Opts │
|
||||
// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│─ ─ ─
|
||||
// │ │ │ │ 5. Verify
|
||||
// │ │ │ PubKey
|
||||
// │ │ │◀─ ─ ┘
|
||||
// │ │ │─ ─ ─
|
||||
// │ │ │ │ 6. Persist
|
||||
// │ │ │ Credential
|
||||
// │ │ │◀─ ─ ┘
|
||||
// │ │ │
|
||||
// │ │ │
|
||||
//
|
||||
// In this step, we are responding to the start reg(istration) request, and providing
|
||||
// the challenge to the browser.
|
||||
|
||||
// TODO - Improve error handling and messages
|
||||
fn auth_user(cookie_content: String, session_table: &mut HashMap<Uuid, Instant>) -> bool {
|
||||
let Ok(uuid) = Uuid::parse_str(&cookie_content) else {
|
||||
info!("此用户Session不在表中");
|
||||
return false;
|
||||
};
|
||||
let Some(expire) = session_table.get(&uuid) else {
|
||||
info!("此用户Session已过期");
|
||||
return false;
|
||||
};
|
||||
if *expire <= Instant::now() {
|
||||
info!("此用户Session已过期");
|
||||
return false;
|
||||
}
|
||||
session_table.insert(
|
||||
uuid,
|
||||
Instant::now() + Duration::from_secs(*SESSION_ACTIVE_TIME),
|
||||
);
|
||||
tracing::info!("valid cookie {}", uuid);
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn start_register(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
cookies: Cookies,
|
||||
session: Session,
|
||||
Path(username): Path<String>,
|
||||
) -> Result<Json<CreationChallengeResponse>, String> {
|
||||
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) {
|
||||
tracing::info!("用户Cookie无效,不在Session表中");
|
||||
return Err(WebauthnError::AuthenticationFailure.to_string());
|
||||
}
|
||||
// Remove any previous registrations that may have occurred from the session.
|
||||
let _ = session
|
||||
.remove::<(String, Uuid, PasskeyRegistration)>("reg_state")
|
||||
.await
|
||||
.map_err(|_| WebauthnError::AuthenticationFailure.to_string())?;
|
||||
let pool = &state.db;
|
||||
let username_tmp = username.clone();
|
||||
let (user_id, exclude_credentials): (Uuid, Option<Vec<CredentialID>>) =
|
||||
match get_uid_by_name(username_tmp, pool).await {
|
||||
Ok(uid) => {
|
||||
let exclude_passkeys = get_credential_from_uid(uid, pool).await?;
|
||||
(
|
||||
uid,
|
||||
Some(
|
||||
exclude_passkeys
|
||||
.into_iter()
|
||||
.map(|k| k.cred_id().clone())
|
||||
.collect(),
|
||||
),
|
||||
)
|
||||
}
|
||||
Err(_) => (Uuid::new_v4(), None),
|
||||
};
|
||||
|
||||
let res = match state.webauthn.start_passkey_registration(
|
||||
user_id,
|
||||
&username,
|
||||
&username,
|
||||
exclude_credentials,
|
||||
) {
|
||||
Ok((ccr, reg_state)) => {
|
||||
// Note that due to the session store in use being a server side memory store, this is
|
||||
// safe to store the reg_state into the session since it is not client controlled and
|
||||
// not open to replay attacks. If this was a cookie store, this would be UNSAFE.
|
||||
session
|
||||
.insert("reg_state", (username, user_id, reg_state))
|
||||
.await
|
||||
.map_err(|_| WebauthnError::ChallengePersistenceError.to_string())?;
|
||||
Json(ccr)
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("challenge_register -> {:?}", e);
|
||||
return Err(WebauthnError::ChallengePersistenceError.to_string());
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// 3. The browser has completed it's steps and the user has created a public key
|
||||
// on their device. Now we have the registration options sent to us, and we need
|
||||
// to verify these and persist them.
|
||||
|
||||
pub async fn finish_register(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
session: Session,
|
||||
Json(reg): Json<RegisterPublicKeyCredential>,
|
||||
) -> Result<impl IntoResponse, String> {
|
||||
info!("完成注册...");
|
||||
let pool = &state.db;
|
||||
let Ok(Some((user_name, user_id, reg_state))) = session
|
||||
.get::<(String, Uuid, PasskeyRegistration)>("reg_state")
|
||||
.await
|
||||
else {
|
||||
return Err(WebauthnError::AuthenticationFailure.to_string()); //Corrupt Session
|
||||
};
|
||||
|
||||
let _ = session
|
||||
.remove::<(Uuid, PasskeyAuthentication)>("reg_state")
|
||||
.await;
|
||||
|
||||
let res = match state.webauthn.finish_passkey_registration(®, ®_state) {
|
||||
Ok(key) => {
|
||||
info!("Passkey 正常");
|
||||
info!("检查用户是否存在");
|
||||
// Check if the user_id already exists
|
||||
let user_count = get_user_count_by_uid(user_id, pool).await?;
|
||||
// If the user doesn't exist, insert them into the users table
|
||||
if user_count == 0
|
||||
&& create_user_if_non_existent(user_id, user_name.to_string(), pool).await? != 1
|
||||
{
|
||||
return Err(WebauthnError::AuthenticationFailure.to_string());
|
||||
}
|
||||
|
||||
// Insert the key into the auth table
|
||||
if add_credential_by_id(user_id, &key, pool).await? != 1 {
|
||||
error!("将用户凭据持久化时失败,rows_affected!=1");
|
||||
return Err(WebauthnError::AuthenticationFailure.to_string());
|
||||
}
|
||||
|
||||
StatusCode::OK
|
||||
}
|
||||
Err(e) => {
|
||||
error!("challenge_register -> {:?}", e);
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// 4. Now that our public key has been registered, we can authenticate a user and verify
|
||||
// that they are the holder of that security token. The work flow is similar to registration.
|
||||
//
|
||||
// ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
// │ Authenticator │ │ Browser │ │ Site │
|
||||
// └───────────────┘ └───────────────┘ └───────────────┘
|
||||
// │ │ │
|
||||
// │ │ 1. Start Auth │
|
||||
// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│
|
||||
// │ │ │
|
||||
// │ │ 2. Challenge │
|
||||
// │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
|
||||
// │ │ │
|
||||
// │ 3. Select Token │ │
|
||||
// ─ ─ ─│◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │
|
||||
// 4. Verify │ │ │ │
|
||||
// │ 4. Yield Sig │ │
|
||||
// └ ─ ─▶│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ │
|
||||
// │ │ 5. Send Auth │
|
||||
// │ │ Opts │
|
||||
// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│─ ─ ─
|
||||
// │ │ │ │ 5. Verify
|
||||
// │ │ │ Sig
|
||||
// │ │ │◀─ ─ ┘
|
||||
// │ │ │
|
||||
// │ │ │
|
||||
//
|
||||
// The user indicates the wish to start authentication and we need to provide a challenge.
|
||||
@ -133,6 +133,5 @@ pub fn check_otp(key_from_db: String, user_input_otp: String) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub mod auth;
|
||||
pub mod cookie_services;
|
||||
pub mod gc_services;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user