webauthn: finish

This commit is contained in:
Félix Baylac Jacqué 2023-02-06 20:26:05 +01:00
parent 448476fda9
commit 80a2048d10
6 changed files with 117 additions and 34 deletions

View File

@ -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())

View File

@ -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(&register, 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)
}

View File

@ -1,6 +1,5 @@
use std::net::SocketAddr;
use clap::Parser;
use warp::Filter;
mod filters;
mod handlers;

View File

@ -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
}
}
}

View File

@ -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>

View File

@ -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);
}) ();