2021-11-07 18:53:39 +01:00
|
|
|
use rocket::serde::json::Json;
|
2021-06-07 23:34:00 +02:00
|
|
|
use rocket::Route;
|
|
|
|
use serde_json::Value;
|
2021-10-24 21:50:26 +02:00
|
|
|
use url::Url;
|
2021-06-07 23:34:00 +02:00
|
|
|
use webauthn_rs::{base64_data::Base64UrlSafeData, proto::*, AuthenticationState, RegistrationState, Webauthn};
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
api::{
|
|
|
|
core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData,
|
|
|
|
},
|
|
|
|
auth::Headers,
|
|
|
|
db::{
|
|
|
|
models::{TwoFactor, TwoFactorType},
|
|
|
|
DbConn,
|
|
|
|
},
|
|
|
|
error::Error,
|
|
|
|
CONFIG,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub fn routes() -> Vec<Route> {
|
|
|
|
routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,]
|
|
|
|
}
|
|
|
|
|
2022-03-27 17:25:04 +02:00
|
|
|
// Some old u2f structs still needed for migrating from u2f to WebAuthn
|
|
|
|
// Both `struct Registration` and `struct U2FRegistration` can be removed if we remove the u2f to WebAuthn migration
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
#[serde(rename_all = "camelCase")]
|
|
|
|
pub struct Registration {
|
|
|
|
pub key_handle: Vec<u8>,
|
|
|
|
pub pub_key: Vec<u8>,
|
|
|
|
pub attestation_cert: Option<Vec<u8>>,
|
|
|
|
pub device_name: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
pub struct U2FRegistration {
|
|
|
|
pub id: i32,
|
|
|
|
pub name: String,
|
|
|
|
#[serde(with = "Registration")]
|
|
|
|
pub reg: Registration,
|
|
|
|
pub counter: u32,
|
|
|
|
compromised: bool,
|
|
|
|
pub migrated: Option<bool>,
|
|
|
|
}
|
|
|
|
|
2021-06-07 23:34:00 +02:00
|
|
|
struct WebauthnConfig {
|
|
|
|
url: String,
|
2021-10-24 21:50:26 +02:00
|
|
|
origin: Url,
|
2021-06-07 23:34:00 +02:00
|
|
|
rpid: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl WebauthnConfig {
|
|
|
|
fn load() -> Webauthn<Self> {
|
|
|
|
let domain = CONFIG.domain();
|
2021-08-30 00:35:25 +02:00
|
|
|
let domain_origin = CONFIG.domain_origin();
|
2021-06-07 23:34:00 +02:00
|
|
|
Webauthn::new(Self {
|
2021-10-27 23:00:26 +02:00
|
|
|
rpid: Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(),
|
2021-06-07 23:34:00 +02:00
|
|
|
url: domain,
|
2021-10-24 21:50:26 +02:00
|
|
|
origin: Url::parse(&domain_origin).unwrap(),
|
2021-06-07 23:34:00 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl webauthn_rs::WebauthnConfig for WebauthnConfig {
|
|
|
|
fn get_relying_party_name(&self) -> &str {
|
|
|
|
&self.url
|
|
|
|
}
|
|
|
|
|
2021-10-24 21:50:26 +02:00
|
|
|
fn get_origin(&self) -> &Url {
|
2021-08-30 00:35:25 +02:00
|
|
|
&self.origin
|
2021-06-07 23:34:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
fn get_relying_party_id(&self) -> &str {
|
|
|
|
&self.rpid
|
|
|
|
}
|
|
|
|
|
2021-07-25 14:49:55 +02:00
|
|
|
/// We have WebAuthn configured to discourage user verification
|
|
|
|
/// if we leave this enabled, it will cause verification issues when a keys send UV=1.
|
|
|
|
/// Upstream (the library they use) ignores this when set to discouraged, so we should too.
|
|
|
|
fn get_require_uv_consistency(&self) -> bool {
|
|
|
|
false
|
2021-06-07 23:34:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
pub struct WebauthnRegistration {
|
|
|
|
pub id: i32,
|
|
|
|
pub name: String,
|
|
|
|
pub migrated: bool,
|
|
|
|
|
|
|
|
pub credential: Credential,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl WebauthnRegistration {
|
|
|
|
fn to_json(&self) -> Value {
|
|
|
|
json!({
|
|
|
|
"Id": self.id,
|
|
|
|
"Name": self.name,
|
|
|
|
"migrated": self.migrated,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/two-factor/get-webauthn", data = "<data>")]
|
2021-11-16 17:07:55 +01:00
|
|
|
async fn get_webauthn(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
2021-06-07 23:34:00 +02:00
|
|
|
if !CONFIG.domain_set() {
|
|
|
|
err!("`DOMAIN` environment variable is not set. Webauthn disabled")
|
|
|
|
}
|
|
|
|
|
|
|
|
if !headers.user.check_valid_password(&data.data.MasterPasswordHash) {
|
|
|
|
err!("Invalid password");
|
|
|
|
}
|
|
|
|
|
2021-11-16 17:07:55 +01:00
|
|
|
let (enabled, registrations) = get_webauthn_registrations(&headers.user.uuid, &conn).await?;
|
2021-06-07 23:34:00 +02:00
|
|
|
let registrations_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();
|
|
|
|
|
|
|
|
Ok(Json(json!({
|
|
|
|
"Enabled": enabled,
|
|
|
|
"Keys": registrations_json,
|
|
|
|
"Object": "twoFactorWebAuthn"
|
|
|
|
})))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
|
2021-11-16 17:07:55 +01:00
|
|
|
async fn generate_webauthn_challenge(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
2021-06-07 23:34:00 +02:00
|
|
|
if !headers.user.check_valid_password(&data.data.MasterPasswordHash) {
|
|
|
|
err!("Invalid password");
|
|
|
|
}
|
|
|
|
|
2021-11-16 17:07:55 +01:00
|
|
|
let registrations = get_webauthn_registrations(&headers.user.uuid, &conn)
|
|
|
|
.await?
|
2021-06-07 23:34:00 +02:00
|
|
|
.1
|
|
|
|
.into_iter()
|
|
|
|
.map(|r| r.credential.cred_id) // We return the credentialIds to the clients to avoid double registering
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let (challenge, state) = WebauthnConfig::load().generate_challenge_register_options(
|
|
|
|
headers.user.uuid.as_bytes().to_vec(),
|
|
|
|
headers.user.email,
|
|
|
|
headers.user.name,
|
|
|
|
Some(registrations),
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
)?;
|
|
|
|
|
|
|
|
let type_ = TwoFactorType::WebauthnRegisterChallenge;
|
2021-11-16 17:07:55 +01:00
|
|
|
TwoFactor::new(headers.user.uuid, type_, serde_json::to_string(&state)?).save(&conn).await?;
|
2021-06-07 23:34:00 +02:00
|
|
|
|
|
|
|
let mut challenge_value = serde_json::to_value(challenge.public_key)?;
|
|
|
|
challenge_value["status"] = "ok".into();
|
|
|
|
challenge_value["errorMessage"] = "".into();
|
|
|
|
Ok(Json(challenge_value))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct EnableWebauthnData {
|
|
|
|
Id: NumberOrString, // 1..5
|
|
|
|
Name: String,
|
|
|
|
MasterPasswordHash: String,
|
|
|
|
DeviceResponse: RegisterPublicKeyCredentialCopy,
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is copied from RegisterPublicKeyCredential to change the Response objects casing
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct RegisterPublicKeyCredentialCopy {
|
|
|
|
pub Id: String,
|
|
|
|
pub RawId: Base64UrlSafeData,
|
|
|
|
pub Response: AuthenticatorAttestationResponseRawCopy,
|
|
|
|
pub Type: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is copied from AuthenticatorAttestationResponseRaw to change clientDataJSON to clientDataJson
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
pub struct AuthenticatorAttestationResponseRawCopy {
|
|
|
|
pub AttestationObject: Base64UrlSafeData,
|
|
|
|
pub ClientDataJson: Base64UrlSafeData,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<RegisterPublicKeyCredentialCopy> for RegisterPublicKeyCredential {
|
|
|
|
fn from(r: RegisterPublicKeyCredentialCopy) -> Self {
|
|
|
|
Self {
|
|
|
|
id: r.Id,
|
|
|
|
raw_id: r.RawId,
|
|
|
|
response: AuthenticatorAttestationResponseRaw {
|
|
|
|
attestation_object: r.Response.AttestationObject,
|
|
|
|
client_data_json: r.Response.ClientDataJson,
|
|
|
|
},
|
|
|
|
type_: r.Type,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is copied from PublicKeyCredential to change the Response objects casing
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
pub struct PublicKeyCredentialCopy {
|
|
|
|
pub Id: String,
|
|
|
|
pub RawId: Base64UrlSafeData,
|
|
|
|
pub Response: AuthenticatorAssertionResponseRawCopy,
|
|
|
|
pub Extensions: Option<AuthenticationExtensionsClientOutputsCopy>,
|
|
|
|
pub Type: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is copied from AuthenticatorAssertionResponseRaw to change clientDataJSON to clientDataJson
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
pub struct AuthenticatorAssertionResponseRawCopy {
|
|
|
|
pub AuthenticatorData: Base64UrlSafeData,
|
|
|
|
pub ClientDataJson: Base64UrlSafeData,
|
|
|
|
pub Signature: Base64UrlSafeData,
|
|
|
|
pub UserHandle: Option<Base64UrlSafeData>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
pub struct AuthenticationExtensionsClientOutputsCopy {
|
|
|
|
#[serde(default)]
|
|
|
|
pub Appid: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
|
|
|
|
fn from(r: PublicKeyCredentialCopy) -> Self {
|
|
|
|
Self {
|
|
|
|
id: r.Id,
|
|
|
|
raw_id: r.RawId,
|
|
|
|
response: AuthenticatorAssertionResponseRaw {
|
|
|
|
authenticator_data: r.Response.AuthenticatorData,
|
|
|
|
client_data_json: r.Response.ClientDataJson,
|
|
|
|
signature: r.Response.Signature,
|
|
|
|
user_handle: r.Response.UserHandle,
|
|
|
|
},
|
|
|
|
extensions: r.Extensions.map(|e| AuthenticationExtensionsClientOutputs {
|
|
|
|
appid: e.Appid,
|
|
|
|
}),
|
|
|
|
type_: r.Type,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/two-factor/webauthn", data = "<data>")]
|
2021-11-16 17:07:55 +01:00
|
|
|
async fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
|
2021-06-07 23:34:00 +02:00
|
|
|
let data: EnableWebauthnData = data.into_inner().data;
|
|
|
|
let mut user = headers.user;
|
|
|
|
|
|
|
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
|
|
|
err!("Invalid password");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retrieve and delete the saved challenge state
|
|
|
|
let type_ = TwoFactorType::WebauthnRegisterChallenge as i32;
|
2021-11-16 17:07:55 +01:00
|
|
|
let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await {
|
2021-06-07 23:34:00 +02:00
|
|
|
Some(tf) => {
|
|
|
|
let state: RegistrationState = serde_json::from_str(&tf.data)?;
|
2021-11-16 17:07:55 +01:00
|
|
|
tf.delete(&conn).await?;
|
2021-06-07 23:34:00 +02:00
|
|
|
state
|
|
|
|
}
|
|
|
|
None => err!("Can't recover challenge"),
|
|
|
|
};
|
|
|
|
|
|
|
|
// Verify the credentials with the saved state
|
|
|
|
let (credential, _data) =
|
|
|
|
WebauthnConfig::load().register_credential(&data.DeviceResponse.into(), &state, |_| Ok(false))?;
|
|
|
|
|
2021-11-16 17:07:55 +01:00
|
|
|
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &conn).await?.1;
|
2021-06-07 23:34:00 +02:00
|
|
|
// TODO: Check for repeated ID's
|
|
|
|
registrations.push(WebauthnRegistration {
|
|
|
|
id: data.Id.into_i32()?,
|
|
|
|
name: data.Name,
|
|
|
|
migrated: false,
|
|
|
|
|
|
|
|
credential,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Save the registrations and return them
|
2021-11-16 17:07:55 +01:00
|
|
|
TwoFactor::new(user.uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?)
|
|
|
|
.save(&conn)
|
|
|
|
.await?;
|
|
|
|
_generate_recover_code(&mut user, &conn).await;
|
2021-06-07 23:34:00 +02:00
|
|
|
|
|
|
|
let keys_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();
|
|
|
|
Ok(Json(json!({
|
|
|
|
"Enabled": true,
|
|
|
|
"Keys": keys_json,
|
|
|
|
"Object": "twoFactorU2f"
|
|
|
|
})))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[put("/two-factor/webauthn", data = "<data>")]
|
2021-11-16 17:07:55 +01:00
|
|
|
async fn activate_webauthn_put(data: JsonUpcase<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
|
|
|
|
activate_webauthn(data, headers, conn).await
|
2021-06-07 23:34:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct DeleteU2FData {
|
|
|
|
Id: NumberOrString,
|
|
|
|
MasterPasswordHash: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[delete("/two-factor/webauthn", data = "<data>")]
|
2021-11-16 17:07:55 +01:00
|
|
|
async fn delete_webauthn(data: JsonUpcase<DeleteU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
|
2021-06-07 23:34:00 +02:00
|
|
|
let id = data.data.Id.into_i32()?;
|
|
|
|
if !headers.user.check_valid_password(&data.data.MasterPasswordHash) {
|
|
|
|
err!("Invalid password");
|
|
|
|
}
|
|
|
|
|
2021-11-16 17:07:55 +01:00
|
|
|
let mut tf = match TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::Webauthn as i32, &conn).await
|
|
|
|
{
|
2021-06-07 23:34:00 +02:00
|
|
|
Some(tf) => tf,
|
|
|
|
None => err!("Webauthn data not found!"),
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut data: Vec<WebauthnRegistration> = serde_json::from_str(&tf.data)?;
|
|
|
|
|
2021-07-25 14:49:55 +02:00
|
|
|
let item_pos = match data.iter().position(|r| r.id == id) {
|
2021-06-07 23:34:00 +02:00
|
|
|
Some(p) => p,
|
|
|
|
None => err!("Webauthn entry not found"),
|
|
|
|
};
|
|
|
|
|
|
|
|
let removed_item = data.remove(item_pos);
|
|
|
|
tf.data = serde_json::to_string(&data)?;
|
2021-11-16 17:07:55 +01:00
|
|
|
tf.save(&conn).await?;
|
2021-06-07 23:34:00 +02:00
|
|
|
drop(tf);
|
|
|
|
|
|
|
|
// If entry is migrated from u2f, delete the u2f entry as well
|
2021-11-16 17:07:55 +01:00
|
|
|
if let Some(mut u2f) = TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::U2f as i32, &conn).await
|
|
|
|
{
|
2021-06-07 23:34:00 +02:00
|
|
|
let mut data: Vec<U2FRegistration> = match serde_json::from_str(&u2f.data) {
|
|
|
|
Ok(d) => d,
|
|
|
|
Err(_) => err!("Error parsing U2F data"),
|
|
|
|
};
|
|
|
|
|
|
|
|
data.retain(|r| r.reg.key_handle != removed_item.credential.cred_id);
|
|
|
|
let new_data_str = serde_json::to_string(&data)?;
|
|
|
|
|
|
|
|
u2f.data = new_data_str;
|
2021-11-16 17:07:55 +01:00
|
|
|
u2f.save(&conn).await?;
|
2021-06-07 23:34:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
let keys_json: Vec<Value> = data.iter().map(WebauthnRegistration::to_json).collect();
|
|
|
|
|
|
|
|
Ok(Json(json!({
|
|
|
|
"Enabled": true,
|
|
|
|
"Keys": keys_json,
|
|
|
|
"Object": "twoFactorU2f"
|
|
|
|
})))
|
|
|
|
}
|
|
|
|
|
2021-11-16 17:07:55 +01:00
|
|
|
pub async fn get_webauthn_registrations(
|
|
|
|
user_uuid: &str,
|
|
|
|
conn: &DbConn,
|
|
|
|
) -> Result<(bool, Vec<WebauthnRegistration>), Error> {
|
2021-06-07 23:34:00 +02:00
|
|
|
let type_ = TwoFactorType::Webauthn as i32;
|
2021-11-16 17:07:55 +01:00
|
|
|
match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await {
|
2021-06-07 23:34:00 +02:00
|
|
|
Some(tf) => Ok((tf.enabled, serde_json::from_str(&tf.data)?)),
|
|
|
|
None => Ok((false, Vec::new())), // If no data, return empty list
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-16 17:07:55 +01:00
|
|
|
pub async fn generate_webauthn_login(user_uuid: &str, conn: &DbConn) -> JsonResult {
|
2021-06-07 23:34:00 +02:00
|
|
|
// Load saved credentials
|
|
|
|
let creds: Vec<Credential> =
|
2021-11-16 17:07:55 +01:00
|
|
|
get_webauthn_registrations(user_uuid, conn).await?.1.into_iter().map(|r| r.credential).collect();
|
2021-06-07 23:34:00 +02:00
|
|
|
|
|
|
|
if creds.is_empty() {
|
|
|
|
err!("No Webauthn devices registered")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a challenge based on the credentials
|
|
|
|
let ext = RequestAuthenticationExtensions::builder().appid(format!("{}/app-id.json", &CONFIG.domain())).build();
|
|
|
|
let (response, state) = WebauthnConfig::load().generate_challenge_authenticate_options(creds, Some(ext))?;
|
|
|
|
|
|
|
|
// Save the challenge state for later validation
|
|
|
|
TwoFactor::new(user_uuid.into(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?)
|
2021-11-16 17:07:55 +01:00
|
|
|
.save(conn)
|
|
|
|
.await?;
|
2021-06-07 23:34:00 +02:00
|
|
|
|
|
|
|
// Return challenge to the clients
|
|
|
|
Ok(Json(serde_json::to_value(response.public_key)?))
|
|
|
|
}
|
|
|
|
|
2021-11-16 17:07:55 +01:00
|
|
|
pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
|
2021-06-07 23:34:00 +02:00
|
|
|
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
|
2021-11-16 17:07:55 +01:00
|
|
|
let state = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await {
|
2021-06-07 23:34:00 +02:00
|
|
|
Some(tf) => {
|
|
|
|
let state: AuthenticationState = serde_json::from_str(&tf.data)?;
|
2021-11-16 17:07:55 +01:00
|
|
|
tf.delete(conn).await?;
|
2021-06-07 23:34:00 +02:00
|
|
|
state
|
|
|
|
}
|
|
|
|
None => err!("Can't recover login challenge"),
|
|
|
|
};
|
|
|
|
|
|
|
|
let rsp: crate::util::UpCase<PublicKeyCredentialCopy> = serde_json::from_str(response)?;
|
|
|
|
let rsp: PublicKeyCredential = rsp.data.into();
|
|
|
|
|
2021-11-16 17:07:55 +01:00
|
|
|
let mut registrations = get_webauthn_registrations(user_uuid, conn).await?.1;
|
2021-06-07 23:34:00 +02:00
|
|
|
|
|
|
|
// If the credential we received is migrated from U2F, enable the U2F compatibility
|
|
|
|
//let use_u2f = registrations.iter().any(|r| r.migrated && r.credential.cred_id == rsp.raw_id.0);
|
|
|
|
let (cred_id, auth_data) = WebauthnConfig::load().authenticate_credential(&rsp, &state)?;
|
|
|
|
|
|
|
|
for reg in &mut registrations {
|
|
|
|
if ®.credential.cred_id == cred_id {
|
|
|
|
reg.credential.counter = auth_data.counter;
|
|
|
|
|
|
|
|
TwoFactor::new(user_uuid.to_string(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?)
|
2021-11-16 17:07:55 +01:00
|
|
|
.save(conn)
|
|
|
|
.await?;
|
2021-06-07 23:34:00 +02:00
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
err!("Credential not present")
|
|
|
|
}
|