Login: full scenario
This commit is contained in:
parent
1eedcf924e
commit
b50e034b8e
|
@ -2,11 +2,11 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1671417167,
|
"lastModified": 1700786208,
|
||||||
"narHash": "sha256-JkHam6WQOwZN1t2C2sbp1TqMv3TVRjzrdoejqfefwrM=",
|
"narHash": "sha256-vP0WI7qNkg3teQJN5xjFcxgnBNiKCbkgw3X9HcAxWJY=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "bb31220cca6d044baa6dc2715b07497a2a7c4bc7",
|
"rev": "8b8c9407844599546393146bfac901290e0ab96b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -eau
|
||||||
|
|
||||||
|
dbname="nomnomdev"
|
||||||
|
port="12345"
|
||||||
|
|
||||||
|
dbdir="$(mktemp -d)"
|
||||||
|
cfgfile="${dbdir}/config.json"
|
||||||
|
trap 'rm -rf ${dbdir}' EXIT
|
||||||
|
|
||||||
|
initdb "$dbdir"
|
||||||
|
postgres -D "${dbdir}" -c unix_socket_directories="${dbdir}" -c listen_addresses= -c port="${port}" &
|
||||||
|
pgpid=$!
|
||||||
|
# Trick to help the "./psql" script to find the DB dir & co
|
||||||
|
cat <<EOF > "/tmp/nom-nom-dev-args"
|
||||||
|
export host="$dbdir"
|
||||||
|
export port="$port"
|
||||||
|
export dbname="$dbname"
|
||||||
|
export cfgfile="$cfgfile"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
trap 'rm -rf ${dbdir} && rm /tmp/nom-nom-dev-args && kill ${pgpid}' EXIT
|
||||||
|
|
||||||
|
# Yeah, this is very meh. We need to wait for the server to be ready
|
||||||
|
#to receive requests to create the DB.
|
||||||
|
sleep 2
|
||||||
|
createdb -h "${dbdir}" -p "${port}" "${dbname}"
|
||||||
|
|
||||||
|
cat <<EOF > "${cfgfile}"
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8001",
|
||||||
|
"db_host": "${dbdir}",
|
||||||
|
"db_port": ${port},
|
||||||
|
"db_name": "${dbname}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cargo build
|
||||||
|
rust-gdb --args ./target/debug/nom-nom-gc-server --bind "[::1]:8001" --config "${cfgfile}"
|
|
@ -1,10 +1,9 @@
|
||||||
|
use actix_web::{web, error, HttpResponse, http::header::ContentType, cookie::{Cookie, SameSite}};
|
||||||
use actix_web::{web, error, HttpResponse, http::header::ContentType};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde::Deserialize;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use webauthn_rs::prelude::{RegisterPublicKeyCredential, PasskeyRegistration};
|
use webauthn_rs::prelude::{RegisterPublicKeyCredential, PasskeyRegistration, Passkey, RequestChallengeResponse, PublicKeyCredential};
|
||||||
|
|
||||||
use crate::{models::{AppState, RegistrationUuid, Key}, templates};
|
use crate::{models::{AppState, RegistrationUuid, Key, LoginUuid, SessionUuid}, templates};
|
||||||
|
|
||||||
/// Page you get after clicking a register-user link.
|
/// Page you get after clicking a register-user link.
|
||||||
///
|
///
|
||||||
|
@ -20,7 +19,7 @@ pub async fn webauthn_registration(app_state: web::Data<AppState<'_>>, path: web
|
||||||
Ok(None) => return HttpResponse::from_error(error::ErrorBadRequest("This registration token is invalid: cannot find it in the database")),
|
Ok(None) => return HttpResponse::from_error(error::ErrorBadRequest("This registration token is invalid: cannot find it in the database")),
|
||||||
Err(e) => return HttpResponse::from_error(error::ErrorInternalServerError(e.to_string())),
|
Err(e) => return HttpResponse::from_error(error::ErrorInternalServerError(e.to_string())),
|
||||||
};
|
};
|
||||||
let keys = match app_state.get_user_keys(&user).await {
|
let keys = match app_state.get_user_keys(&user.uuid).await {
|
||||||
Ok(keys) => keys.into_iter().map(|k| k.name).collect(),
|
Ok(keys) => keys.into_iter().map(|k| k.name).collect(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("webauthn_registration: error while retrieving keys: {}", e);
|
eprintln!("webauthn_registration: error while retrieving keys: {}", e);
|
||||||
|
@ -34,7 +33,7 @@ pub async fn webauthn_registration(app_state: web::Data<AppState<'_>>, path: web
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct InitBody {
|
pub struct InitRegistrationBody {
|
||||||
uuid: RegistrationUuid
|
uuid: RegistrationUuid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +42,7 @@ pub struct InitBody {
|
||||||
/// For now, we don't save anything to the DB. We just generate the
|
/// For now, we don't save anything to the DB. We just generate the
|
||||||
/// challenge, the UUID and store everything in the session hashmap
|
/// challenge, the UUID and store everything in the session hashmap
|
||||||
/// server-side, in the response and cookie on the client-side.
|
/// server-side, in the response and cookie on the client-side.
|
||||||
pub async fn start_webauthn_registration(app_state: web::Data<AppState<'_>>, body: web::Json<InitBody>) -> HttpResponse {
|
pub async fn start_webauthn_registration(app_state: web::Data<AppState<'_>>, body: web::Json<InitRegistrationBody>) -> HttpResponse {
|
||||||
let res = app_state.retrieve_registration_user(&body.uuid).await;
|
let res = app_state.retrieve_registration_user(&body.uuid).await;
|
||||||
let user = match res {
|
let user = match res {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -77,8 +76,10 @@ pub struct RegisterWebauthnPayload {
|
||||||
/// We verify the key enrolling challenge, then store the new key in the DB.
|
/// We verify the key enrolling challenge, then store the new key in the DB.
|
||||||
pub async fn finish_webauthn_registration(app_state: web::Data<AppState<'_>>, register: web::Json<RegisterWebauthnPayload>) -> impl actix_web::Responder {
|
pub async fn finish_webauthn_registration(app_state: web::Data<AppState<'_>>, register: web::Json<RegisterWebauthnPayload>) -> impl actix_web::Responder {
|
||||||
let passkey_registration: Option<PasskeyRegistration> = {
|
let passkey_registration: Option<PasskeyRegistration> = {
|
||||||
let user_registrations = app_state.session.user_registrations.read().await;
|
let mut user_registrations = app_state.session.user_registrations.write().await;
|
||||||
user_registrations.get(®ister.uuid).cloned()
|
let pr = user_registrations.get(®ister.uuid).cloned();
|
||||||
|
user_registrations.remove(®ister.uuid);
|
||||||
|
pr
|
||||||
};
|
};
|
||||||
let db_usr = app_state.retrieve_registration_user(®ister.uuid).await;
|
let db_usr = app_state.retrieve_registration_user(®ister.uuid).await;
|
||||||
let user = match db_usr {
|
let user = match db_usr {
|
||||||
|
@ -105,7 +106,6 @@ pub async fn finish_webauthn_registration(app_state: web::Data<AppState<'_>>, re
|
||||||
// user? Maybe rather later, when we're sure we
|
// user? Maybe rather later, when we're sure we
|
||||||
// have all the keys required.
|
// have all the keys required.
|
||||||
app_state.save_user_key(&user.uuid, &passkey).await.unwrap();
|
app_state.save_user_key(&user.uuid, &passkey).await.unwrap();
|
||||||
// app_state.delete_registration_link(®ister.uuid.clone()).await.unwrap();
|
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.body("ok")
|
.body("ok")
|
||||||
},
|
},
|
||||||
|
@ -119,3 +119,108 @@ pub async fn finish_webauthn_registration(app_state: web::Data<AppState<'_>>, re
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn webauthn_login(app_state: web::Data<AppState<'_>>) -> impl actix_web::Responder {
|
||||||
|
let response = templates::login(app_state.hbs.clone()).unwrap();
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type(ContentType::html())
|
||||||
|
.body(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct InitLoginBody {
|
||||||
|
username: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
pub struct InitLoginResp {
|
||||||
|
uuid: LoginUuid,
|
||||||
|
challenge: RequestChallengeResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn webauthn_login_init(app_state: web::Data<AppState<'_>>, body: web::Json<InitLoginBody>) -> impl actix_web::Responder {
|
||||||
|
let Ok(user) = app_state.get_user(&body.username).await else {
|
||||||
|
return HttpResponse::from_error(error::ErrorBadRequest("Invalid username"))
|
||||||
|
};
|
||||||
|
let Ok(keys) = app_state.get_user_keys(&user.uuid).await else {
|
||||||
|
return HttpResponse::from_error(error::ErrorInternalServerError("Cannot find keys attached to user"));
|
||||||
|
};
|
||||||
|
let keys: Vec<Passkey> = keys.into_iter().map(|k| k.key).collect();
|
||||||
|
let (challenge, passkey_auth) = match app_state.webauthn.start_passkey_authentication(keys.as_slice()) {
|
||||||
|
Ok((c, p)) => (c, p),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("webauthn_login_init: webauthn error: {}", e);
|
||||||
|
return HttpResponse::from_error(error::ErrorInternalServerError("Webauthn error"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let uuid = LoginUuid(Uuid::new_v4());
|
||||||
|
{
|
||||||
|
let mut user_logins = app_state.session.user_pending_logins.write().await;
|
||||||
|
user_logins.insert(uuid.clone(), (passkey_auth, user));
|
||||||
|
}
|
||||||
|
let res = InitLoginResp {
|
||||||
|
uuid,
|
||||||
|
challenge,
|
||||||
|
};
|
||||||
|
let res = serde_json::to_string(&res).unwrap();
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type(ContentType::json())
|
||||||
|
.body(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct FinishLoginBody {
|
||||||
|
challenge: PublicKeyCredential,
|
||||||
|
uuid: LoginUuid
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn webauthn_login_finish(app_state: web::Data<AppState<'_>>, body: web::Json<FinishLoginBody>) -> impl actix_web::Responder {
|
||||||
|
let passkey_auth = {
|
||||||
|
let mut user_logins = app_state.session.user_pending_logins.write().await;
|
||||||
|
let pk = user_logins.get(&body.uuid).cloned();
|
||||||
|
user_logins.remove(&body.uuid);
|
||||||
|
pk
|
||||||
|
};
|
||||||
|
let Some((passkey_auth,user)) = passkey_auth else {
|
||||||
|
eprintln!("webauthn_login_finish: cannot find login state");
|
||||||
|
return HttpResponse::from_error(error::ErrorBadRequest("Cannot find login state in session"));
|
||||||
|
};
|
||||||
|
let res = match app_state.webauthn.finish_passkey_authentication(&body.challenge, &passkey_auth) {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("webauthn_login_finish: webauthn error: {}", e);
|
||||||
|
return HttpResponse::from_error(error::ErrorForbidden("Failed webauthn auth"));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let session_uuid = {
|
||||||
|
let mut user_sessions = app_state.session.user_sessions.write().await;
|
||||||
|
let session_uuid = SessionUuid(Uuid::new_v4());
|
||||||
|
user_sessions.insert(session_uuid, user.clone());
|
||||||
|
session_uuid
|
||||||
|
};
|
||||||
|
if res.needs_update() {
|
||||||
|
match app_state.update_user_keys(&user.uuid, res).await {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error while updating the key states: {}", e);
|
||||||
|
return HttpResponse::from_error(error::ErrorInternalServerError("Error while updating the key states."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let response = templates::login(app_state.hbs.clone()).unwrap();
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let secure = false;
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let secure = true;
|
||||||
|
let auth_cookie = Cookie::build("auth-uuid", session_uuid.0.to_string())
|
||||||
|
.http_only(true)
|
||||||
|
.same_site(SameSite::Strict)
|
||||||
|
.secure(secure)
|
||||||
|
.finish();
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type(ContentType::html())
|
||||||
|
.cookie(auth_cookie)
|
||||||
|
.body(response)
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{self, Value};
|
use serde_json::{self, Value};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tokio_postgres::NoTls;
|
use tokio_postgres::NoTls;
|
||||||
use webauthn_rs::prelude::{Uuid, PasskeyRegistration, Passkey};
|
use webauthn_rs::prelude::{Uuid, PasskeyRegistration, Passkey, PasskeyAuthentication, AuthenticationResult};
|
||||||
use webauthn_rs::{Webauthn, WebauthnBuilder};
|
use webauthn_rs::{Webauthn, WebauthnBuilder};
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
@ -36,16 +36,23 @@ pub type DbField<T> = Arc<RwLock<T>>;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TempSession {
|
pub struct TempSession {
|
||||||
pub user_registrations: Arc<RwLock<HashMap<RegistrationUuid,PasskeyRegistration>>>
|
pub user_registrations: Arc<RwLock<HashMap<RegistrationUuid,PasskeyRegistration>>>,
|
||||||
|
pub user_pending_logins: Arc<RwLock<HashMap<LoginUuid,(PasskeyAuthentication, User)>>>,
|
||||||
|
pub user_sessions: Arc<RwLock<HashMap<SessionUuid,User>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct RegistrationUuid(pub Uuid);
|
pub struct RegistrationUuid(pub Uuid);
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct LoginUuid(pub Uuid);
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Hash, FromSql, ToSql)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Hash, FromSql, ToSql)]
|
||||||
#[postgres(transparent)]
|
#[postgres(transparent)]
|
||||||
pub struct UserUuid(pub Uuid);
|
pub struct UserUuid(pub Uuid);
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
|
pub struct SessionUuid(pub Uuid);
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
|
@ -81,7 +88,9 @@ impl AppState<'_> {
|
||||||
let webauthn = Arc::new(builder.build().expect("Invalid configuration"));
|
let webauthn = Arc::new(builder.build().expect("Invalid configuration"));
|
||||||
let hbs = Arc::new(crate::templates::new().unwrap());
|
let hbs = Arc::new(crate::templates::new().unwrap());
|
||||||
let session: TempSession = TempSession {
|
let session: TempSession = TempSession {
|
||||||
user_registrations: Arc::new(RwLock::new(HashMap::new()))
|
user_registrations: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
user_pending_logins: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
user_sessions: Arc::new(RwLock::new(HashMap::new()))
|
||||||
};
|
};
|
||||||
let mut pg_config = tokio_postgres::Config::new();
|
let mut pg_config = tokio_postgres::Config::new();
|
||||||
pg_config.host_path(conf.db_host.unwrap().clone());
|
pg_config.host_path(conf.db_host.unwrap().clone());
|
||||||
|
@ -153,6 +162,14 @@ impl AppState<'_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_user(&self, username: &str) -> Result<User> {
|
||||||
|
let mut conn = self.db.get().await?;
|
||||||
|
let client = conn.deref_mut();
|
||||||
|
let stmt = client.prepare_cached("SELECT id, user_name FROM Users WHERE user_name = $1").await?;
|
||||||
|
let row = client.query_one(&stmt, &[&username]).await?;
|
||||||
|
Ok(User{ uuid: row.get(0), name: row.get(1) })
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn save_user(&self, user: &User) -> Result<()> {
|
pub async fn save_user(&self, user: &User) -> Result<()> {
|
||||||
let mut conn = self.db.get().await?;
|
let mut conn = self.db.get().await?;
|
||||||
let client = conn.deref_mut();
|
let client = conn.deref_mut();
|
||||||
|
@ -170,11 +187,11 @@ impl AppState<'_> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_keys(&self, user: &User) -> Result<Vec<Key>> {
|
pub async fn get_user_keys(&self, user_id: &UserUuid) -> Result<Vec<Key>> {
|
||||||
let mut conn = self.db.get().await?;
|
let mut conn = self.db.get().await?;
|
||||||
let client = conn.deref_mut();
|
let client = conn.deref_mut();
|
||||||
let stmt = client.prepare_cached("SELECT name, key_dump FROM Keys WHERE user_id = $1").await?;
|
let stmt = client.prepare_cached("SELECT name, key_dump FROM Keys WHERE user_id = $1").await?;
|
||||||
let res = client.query(&stmt, &[&user.uuid.0]).await?;
|
let res = client.query(&stmt, &[&user_id.0]).await?;
|
||||||
let res = res.iter().map(|row| {
|
let res = res.iter().map(|row| {
|
||||||
let key: Value = row.get(1);
|
let key: Value = row.get(1);
|
||||||
Key {
|
Key {
|
||||||
|
@ -186,4 +203,40 @@ impl AppState<'_> {
|
||||||
}}).collect();
|
}}).collect();
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Updates the keys for the user `UserUuid` according to the last
|
||||||
|
successful login `AuthenticationResult`.
|
||||||
|
*/
|
||||||
|
pub async fn update_user_keys(&self, user_id: &UserUuid, auth_res: AuthenticationResult) -> Result<()> {
|
||||||
|
let mut conn = self.db.get().await?;
|
||||||
|
let transaction = conn.deref_mut().transaction().await?;
|
||||||
|
|
||||||
|
// Retrieve user keys
|
||||||
|
let stmt = transaction.prepare_cached("SELECT name, key_dump FROM Keys WHERE user_id = $1").await?;
|
||||||
|
let res = transaction.query(&stmt, &[&user_id.0]).await?;
|
||||||
|
let mut keys: Vec<Key> = res.iter().map(|row| {
|
||||||
|
let key = row.get(1);
|
||||||
|
Key { name: row.get(0), key: serde_json::from_value(key).unwrap() }
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
// Update keys
|
||||||
|
keys.iter_mut().for_each(
|
||||||
|
|key| {
|
||||||
|
key.key.update_credential(&auth_res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete current keys, save updated keys
|
||||||
|
let stmt = transaction.prepare_cached("DELETE FROM Keys WHERE user_id = $1").await?;
|
||||||
|
let _ = transaction.execute(&stmt, &[&user_id.0]).await?;
|
||||||
|
let stmt = transaction.prepare_cached("INSERT INTO Keys (user_id, name, key_dump) VALUES ($1,$2,$3)").await?;
|
||||||
|
for key in keys {
|
||||||
|
let passkey_val: Value = serde_json::to_value(&key.key).unwrap();
|
||||||
|
let _ = transaction.execute(&stmt, &[&user_id.0, &key.name, &passkey_val]).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,9 @@ async fn main() -> std::io::Result<()> {
|
||||||
.route("/account/register/{uuid}", web::get().to(handlers::webauthn_registration))
|
.route("/account/register/{uuid}", web::get().to(handlers::webauthn_registration))
|
||||||
.route("/account/register-init", web::post().to(handlers::start_webauthn_registration))
|
.route("/account/register-init", web::post().to(handlers::start_webauthn_registration))
|
||||||
.route("/account/register-finish", web::post().to(handlers::finish_webauthn_registration))
|
.route("/account/register-finish", web::post().to(handlers::finish_webauthn_registration))
|
||||||
|
.route("/login", web::get().to(handlers::webauthn_login))
|
||||||
|
.route("/login/init", web::post().to(handlers::webauthn_login_init))
|
||||||
|
.route("/login/finish", web::post().to(handlers::webauthn_login_finish))
|
||||||
})
|
})
|
||||||
.bind(addr)
|
.bind(addr)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
{{#> template js=js }}
|
||||||
|
<h1>Login</h1>
|
||||||
|
<form id="key-form"><label>Username</label><input id="user-name"/><input type="submit" value="Login" id="login-btn"/></form>
|
||||||
|
{{ /template }}
|
|
@ -13,13 +13,18 @@ pub fn new<'a>() -> Result<Handlebars<'a>, RenderError> {
|
||||||
let register_user = rootpath.join("src/templates/register-user.hbs");
|
let register_user = rootpath.join("src/templates/register-user.hbs");
|
||||||
let mut hbs = handlebars::Handlebars::new();
|
let mut hbs = handlebars::Handlebars::new();
|
||||||
let css = rootpath.join("src/templates/main.css");
|
let css = rootpath.join("src/templates/main.css");
|
||||||
let webauthn_js = rootpath.join("src/templates/webauthn.js");
|
let webauthn_register_js = rootpath.join("src/templates/webauthn-register.js");
|
||||||
|
let webauthn_login_js = rootpath.join("src/templates/webauthn-login.js");
|
||||||
|
let login = rootpath.join("src/templates/login.hbs");
|
||||||
|
|
||||||
hbs.register_template_file("landing", landing_path.to_str().unwrap())?;
|
hbs.register_template_file("landing", landing_path.to_str().unwrap())?;
|
||||||
hbs.register_template_file("template", template_path.to_str().unwrap())?;
|
hbs.register_template_file("template", template_path.to_str().unwrap())?;
|
||||||
hbs.register_template_file("css", css.to_str().unwrap())?;
|
hbs.register_template_file("css", css.to_str().unwrap())?;
|
||||||
hbs.register_template_file("webauthn-js", webauthn_js.to_str().unwrap())?;
|
hbs.register_template_file("webauthn-register-js", webauthn_register_js.to_str().unwrap())?;
|
||||||
|
hbs.register_template_file("webauthn-login-js", webauthn_login_js.to_str().unwrap())?;
|
||||||
hbs.register_template_file("register-user", register_user.to_str().unwrap())?;
|
hbs.register_template_file("register-user", register_user.to_str().unwrap())?;
|
||||||
|
hbs.register_template_file("login", login.to_str().unwrap())?;
|
||||||
|
|
||||||
Ok(hbs)
|
Ok(hbs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +39,7 @@ pub fn register_user_start(hb: Arc<Handlebars<'_>>, registration_uuid: Registrat
|
||||||
let js_data = json!({
|
let js_data = json!({
|
||||||
"registration-uuid": ®istration_uuid.0.to_string(),
|
"registration-uuid": ®istration_uuid.0.to_string(),
|
||||||
});
|
});
|
||||||
let js = hb.render("webauthn-js", &js_data)?;
|
let js = hb.render("webauthn-register-js", &js_data)?;
|
||||||
let data = json!({
|
let data = json!({
|
||||||
"js": js,
|
"js": js,
|
||||||
"username": &username,
|
"username": &username,
|
||||||
|
@ -42,3 +47,14 @@ pub fn register_user_start(hb: Arc<Handlebars<'_>>, registration_uuid: Registrat
|
||||||
});
|
});
|
||||||
hb.render("register-user", &data)
|
hb.render("register-user", &data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn login(hb: Arc<Handlebars<'_>>, ) -> Result<String, RenderError> {
|
||||||
|
let js_data = json!({
|
||||||
|
});
|
||||||
|
let js = hb.render("webauthn-login-js", &js_data)?;
|
||||||
|
let data = json!({
|
||||||
|
"js": js,
|
||||||
|
});
|
||||||
|
hb.render("login", &data)
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{{#> template js=js }}
|
{{#> template js=js }}
|
||||||
<h1>New user: {{ username }}</h1>
|
<h1>New user: {{ username }}</h1>
|
||||||
<div id="key-form"><label>Key Name</label><input id="key-name"/><button id="enroll-key-btn">Enroll FIDO key</button></div>
|
<form id="key-form"><label>Key Name</label><input id="key-name"/><input type="submit" value="Enroll FIDO key"/></form>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>KeyID</th></tr>
|
<tr><th>KeyID</th></tr>
|
||||||
{{#each keyids}}
|
{{#each keyids}}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
async function fetch_challenge () {
|
||||||
|
const username = document.getElementById("user-name").value;
|
||||||
|
const init_data = {
|
||||||
|
username: username,
|
||||||
|
};
|
||||||
|
const challenge = await fetch("/login/init", {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(init_data),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return await challenge.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function solve_challenge (challenge) {
|
||||||
|
// Decode challenge and credential ID base64 strings into JS arrays.
|
||||||
|
challenge.publicKey.challenge = Base64.toUint8Array(challenge.publicKey.challenge);
|
||||||
|
challenge.publicKey.allowCredentials = challenge.publicKey.allowCredentials.map((cred) => {
|
||||||
|
return Object.assign({}, cred, {
|
||||||
|
id: Base64.toUint8Array(cred.id)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return navigator.credentials.get(challenge);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeSolvedChallenge(solvedChallenge) {
|
||||||
|
const uinttobase64 = (array) => Base64.fromUint8Array(new Uint8Array(array), true);
|
||||||
|
return {
|
||||||
|
id: solvedChallenge.id,
|
||||||
|
rawId: uinttobase64(solvedChallenge.rawId),
|
||||||
|
response: {
|
||||||
|
authenticatorData: uinttobase64(solvedChallenge.response.authenticatorData),
|
||||||
|
clientDataJSON: uinttobase64(solvedChallenge.response.clientDataJSON),
|
||||||
|
signature: uinttobase64(solvedChallenge.response.signature),
|
||||||
|
userHandle: solvedChallenge.response.userHandle
|
||||||
|
},
|
||||||
|
type: solvedChallenge.type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function finish_auth (challengeUuid, solvedChallenge) {
|
||||||
|
// Encode challenge response
|
||||||
|
console.log(solvedChallenge);
|
||||||
|
const encodedSolvedChallenge = encodeSolvedChallenge(solvedChallenge);
|
||||||
|
console.log(encodedSolvedChallenge);
|
||||||
|
const resp = await fetch("/login/finish", {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({uuid: challengeUuid, challenge: encodedSolvedChallenge}),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function perform_webauthn_dance () {
|
||||||
|
const challenge = await fetch_challenge()
|
||||||
|
let solved = await solve_challenge(challenge.challenge);
|
||||||
|
let finished = await finish_auth(challenge.uuid, solved);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main
|
||||||
|
(async () => {
|
||||||
|
const button = document.getElementById("key-form");
|
||||||
|
button.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await perform_webauthn_dance();
|
||||||
|
});
|
||||||
|
}) ();
|
|
@ -40,19 +40,21 @@ const finish_auth = async (solvedChallenge) => {
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
});
|
});
|
||||||
console.log(resp);
|
return resp;
|
||||||
return resp.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const perform_webauthn_dance = async () => {
|
const perform_webauthn_dance = async () => {
|
||||||
const publicKey = await start_webauthn()
|
const publicKey = await start_webauthn()
|
||||||
console.log(publicKey);
|
|
||||||
let solved = await solve_challenge(publicKey);
|
let solved = await solve_challenge(publicKey);
|
||||||
let finished = await finish_auth(solved);
|
let finished = await finish_auth(solved);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main
|
// Main
|
||||||
(async () => {
|
(async () => {
|
||||||
const button = document.getElementById("enroll-key-btn");
|
const form = document.getElementById("key-form");
|
||||||
button.addEventListener("click", perform_webauthn_dance);
|
form.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await perform_webauthn_dance();
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
}) ();
|
}) ();
|
Loading…
Reference in New Issue