webauthn: finish
This commit is contained in:
parent
448476fda9
commit
80a2048d10
|
@ -1,30 +1,41 @@
|
|||
use serde::de::DeserializeOwned;
|
||||
use warp::Filter;
|
||||
use webauthn_rs::prelude::RegisterPublicKeyCredential;
|
||||
|
||||
use crate::handlers;
|
||||
use crate::models;
|
||||
|
||||
|
||||
pub fn all(state: models::AppState) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone + '_{
|
||||
pub fn all(state: models::AppState) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone + '_{
|
||||
landing(state.clone())
|
||||
.or(start_webauthn_registration(state.clone()))
|
||||
.or(start_webauthn_registration(state.clone()))
|
||||
.or(finish_webauthn_registration(state.clone()))
|
||||
}
|
||||
|
||||
pub fn landing(state: models::AppState) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone + '_ {
|
||||
pub fn landing(state: models::AppState) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone + '_ {
|
||||
warp::path::end()
|
||||
.and(warp::any().map(move || state.clone()))
|
||||
.and_then(handlers::landing_page)
|
||||
}
|
||||
|
||||
pub fn start_webauthn_registration(state: models::AppState) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone + '_ {
|
||||
pub fn start_webauthn_registration<'a>(state: models::AppState<'a>) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone + 'a {
|
||||
warp::path!("account" / "register-init")
|
||||
.and(warp::post())
|
||||
.and(json_body())
|
||||
.and(json_body::<models::User>())
|
||||
.and(warp::any().map(move || state.clone()))
|
||||
.and_then(handlers::start_webauthn_registration)
|
||||
}
|
||||
|
||||
fn json_body() -> impl Filter<Extract = (models::User,), Error = warp::Rejection> + Clone {
|
||||
pub fn finish_webauthn_registration<'a>(state: models::AppState<'a>) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone + 'a {
|
||||
warp::path!("account" / "register-finish")
|
||||
.and(warp::post())
|
||||
.and(json_body::<RegisterPublicKeyCredential>())
|
||||
.and(warp::any().map(move || state.clone()))
|
||||
.and(warp::cookie("uuid"))
|
||||
.and_then(handlers::finish_webauthn_registration)
|
||||
}
|
||||
|
||||
fn json_body<'a, M: Send + DeserializeOwned>() -> impl Filter<Extract = (M,), Error = warp::Rejection> + Clone {
|
||||
// When accepting a body, we want a JSON body
|
||||
// (and to reject huge payloads)...
|
||||
warp::body::content_length_limit(1024 * 16).and(warp::body::json())
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::convert::Infallible;
|
||||
use warp::Reply;
|
||||
use webauthn_rs::prelude::{RegisterPublicKeyCredential, Uuid};
|
||||
|
||||
use crate::{models::{AppState, User}, templates};
|
||||
|
||||
|
@ -9,11 +10,36 @@ pub async fn landing_page (app_state: AppState<'_>) -> Result<impl Reply, Infall
|
|||
}
|
||||
|
||||
pub async fn start_webauthn_registration(user: User, app_state: AppState<'_>) -> Result<impl warp::Reply, Infallible> {
|
||||
let mut db = app_state.db.lock().await;
|
||||
// TODO: query the user
|
||||
let user = db.users.first_mut().unwrap();
|
||||
|
||||
let (creation_challenge_response, passkey_registration) = app_state.webauthn.start_passkey_registration(user.uuid, &user.user_name, &user.display_name, None).unwrap();
|
||||
|
||||
Ok(warp::reply::json(&creation_challenge_response))
|
||||
let uuid_str = user.uuid.to_string();
|
||||
{
|
||||
let mut session = app_state.session.user_registrations.write().await;
|
||||
session.insert(user.clone(), passkey_registration);
|
||||
}
|
||||
{
|
||||
let mut uuid_db = app_state.db.user_uuid_object.write().await;
|
||||
uuid_db.insert(user.uuid, user);
|
||||
}
|
||||
let json_reply = warp::reply::json(&creation_challenge_response);
|
||||
Ok(warp::reply::with_header(json_reply, "Set-Cookie", format!("uuid={};SameSite=Strict", &uuid_str)))
|
||||
}
|
||||
|
||||
pub async fn finish_webauthn_registration(register: RegisterPublicKeyCredential, app_state: AppState<'_>, uuid: Uuid) -> Result<impl warp::Reply, Infallible> {
|
||||
let registration_result = {
|
||||
let users = app_state.db.user_uuid_object.read().await;
|
||||
let user = users.get(&uuid).unwrap();
|
||||
let session = app_state.session.user_registrations.read().await;
|
||||
let passkey_registration = session.get(&user).unwrap();
|
||||
app_state.webauthn.finish_passkey_registration(®ister, passkey_registration)
|
||||
};
|
||||
let reply = {
|
||||
let mut user_keys = app_state.db.user_keys.write().await;
|
||||
registration_result.map_or(
|
||||
warp::reply::with_status("Challenge failed, cannot register key", warp::http::StatusCode::UNAUTHORIZED),
|
||||
|passkey| {
|
||||
user_keys.insert(uuid, passkey);
|
||||
warp::reply::with_status("ok",warp::http::StatusCode::OK)
|
||||
})
|
||||
};
|
||||
Ok(reply)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use std::net::SocketAddr;
|
||||
use clap::Parser;
|
||||
use warp::Filter;
|
||||
|
||||
mod filters;
|
||||
mod handlers;
|
||||
|
|
|
@ -1,20 +1,28 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use std::sync::{Arc};
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use tokio::sync::{Mutex};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use handlebars::Handlebars;
|
||||
use webauthn_rs::prelude::Uuid;
|
||||
use webauthn_rs::prelude::{Uuid, PasskeyRegistration, Passkey};
|
||||
use webauthn_rs::{Webauthn, WebauthnBuilder};
|
||||
|
||||
pub type Db = Arc<Mutex<Database>>;
|
||||
pub type DbField<T> = Arc<RwLock<T>>;
|
||||
|
||||
pub struct Database {
|
||||
pub users: Vec<User>
|
||||
#[derive(Clone)]
|
||||
pub struct Db {
|
||||
pub user_keys: DbField<HashMap<Uuid, Passkey>>,
|
||||
pub user_uuid_object: DbField<HashMap<Uuid, User>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct TempSession {
|
||||
pub user_registrations: Arc<RwLock<HashMap<User,PasskeyRegistration>>>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct User {
|
||||
pub uuid: Uuid,
|
||||
pub user_name: String,
|
||||
|
@ -25,7 +33,8 @@ pub struct User {
|
|||
pub struct AppState<'a>{
|
||||
pub webauthn: Arc<Webauthn>,
|
||||
pub db: Db,
|
||||
pub hbs: Arc<Handlebars<'a>>
|
||||
pub hbs: Arc<Handlebars<'a>>,
|
||||
pub session: TempSession
|
||||
}
|
||||
|
||||
impl AppState<'_> {
|
||||
|
@ -35,20 +44,21 @@ impl AppState<'_> {
|
|||
let builder = WebauthnBuilder::new(rp, &rp_origin).expect("Invalid configuration");
|
||||
let builder = builder.rp_name("LocalHost");
|
||||
let webauthn = Arc::new(builder.build().expect("Invalid configuration"));
|
||||
let user: User = User {
|
||||
uuid: Uuid::new_v4(),
|
||||
user_name: "felix".to_string(),
|
||||
display_name: "Félix".to_string(),
|
||||
let db: Db = Db {
|
||||
user_keys: Arc::new(RwLock::new(HashMap::new())),
|
||||
user_uuid_object: Arc::new(RwLock::new(HashMap::new()))
|
||||
|
||||
};
|
||||
let db: Db = Arc::new(Mutex::new(Database {
|
||||
users: Vec::from([user])
|
||||
}));
|
||||
let hbs = Arc::new(crate::templates::new().unwrap());
|
||||
let session: TempSession = TempSession {
|
||||
user_registrations: Arc::new(RwLock::new(HashMap::new()))
|
||||
};
|
||||
|
||||
AppState {
|
||||
webauthn,
|
||||
db,
|
||||
hbs
|
||||
hbs,
|
||||
session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,12 @@
|
|||
<head>
|
||||
<title>Nom Nom GC</title>
|
||||
<style>{{> css}}</style>
|
||||
</head>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/js-base64@3.7.4/base64.min.js"
|
||||
integrity="sha384-VkKbwLiG7C18stSGuvcw9W0BHk45Ba7P9LJG5c01Yo4BI6qhFoWSa9TQLNA6EOzI"
|
||||
crossorigin="anonymous">
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<ul>
|
||||
|
|
|
@ -16,15 +16,47 @@ const start_webauthn = async () => {
|
|||
|
||||
const solve_challenge = async (publicKey) => {
|
||||
const encoder = new TextEncoder();
|
||||
publicKey.publicKey.challenge = encoder.encode(publicKey.publicKey.challenge);
|
||||
publicKey.publicKey.user.id = encoder.encode(publicKey.publicKey.user.id);
|
||||
return await navigator.credentials.create(publicKey);
|
||||
publicKey.publicKey.challenge = Base64.toUint8Array(publicKey.publicKey.challenge);
|
||||
publicKey.publicKey.user.id = Base64.toUint8Array(publicKey.publicKey.user.id);
|
||||
return navigator.credentials.create(publicKey);
|
||||
}
|
||||
|
||||
const finish_auth = async (solvedChallenge) => {
|
||||
const encodeArray = (array) => Base64.fromUint8Array(new Uint8Array(array), true);
|
||||
const encodeArray2 = (array) => btoa(String.fromCharCode(...new Uint8Array(array)));
|
||||
console.log(solvedChallenge);
|
||||
const encodedSolvedChallenge = {
|
||||
id: solvedChallenge.id,
|
||||
rawId: encodeArray(solvedChallenge.rawId),
|
||||
response: {
|
||||
clientDataJSON: encodeArray(solvedChallenge.response.clientDataJSON),
|
||||
attestationObject: encodeArray(solvedChallenge.response.attestationObject)
|
||||
},
|
||||
type: solvedChallenge.type
|
||||
};
|
||||
const encodedSolvedChallenge2 = {
|
||||
id: solvedChallenge.id,
|
||||
rawId: encodeArray2(solvedChallenge.rawId),
|
||||
response: {
|
||||
clientDataJSON: encodeArray2(solvedChallenge.response.clientDataJSON),
|
||||
attestationObject: encodeArray2(solvedChallenge.response.attestationObject)
|
||||
},
|
||||
type: solvedChallenge.type
|
||||
};
|
||||
console.log(encodedSolvedChallenge);
|
||||
console.log(encodedSolvedChallenge2);
|
||||
const resp = await fetch("/account/register-finish", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(encodedSolvedChallenge),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
// Main
|
||||
(async () => {
|
||||
const publicKey = await start_webauthn()
|
||||
let solved = await solve_challenge(publicKey);
|
||||
// TODO: send back to server
|
||||
let finished = await finish_auth(solved);
|
||||
console.log(solved);
|
||||
}) ();
|
||||
|
|
Loading…
Reference in New Issue