mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-07-09 22:14:58 +00:00
implement working webauthn login
* without encryption not implemented * deletion not implemented * does not handle errors well
This commit is contained in:
parent
0d3f283c37
commit
f5bf0edf8c
11 changed files with 998 additions and 22 deletions
|
@ -0,0 +1 @@
|
||||||
|
-- This file should undo anything in `up.sql`
|
|
@ -0,0 +1,11 @@
|
||||||
|
CREATE TABLE web_authn_credentials (
|
||||||
|
uuid TEXT NOT NULL PRIMARY KEY,
|
||||||
|
user_uuid TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
credential TEXT NOT NULL,
|
||||||
|
supports_prf BOOLEAN NOT NULL,
|
||||||
|
encrypted_user_key TEXT NOT NULL,
|
||||||
|
encrypted_public_key TEXT NOT NULL,
|
||||||
|
encrypted_private_key TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(user_uuid) REFERENCES users(uuid)
|
||||||
|
);
|
|
@ -8,6 +8,9 @@ mod public;
|
||||||
mod sends;
|
mod sends;
|
||||||
pub mod two_factor;
|
pub mod two_factor;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
use crate::db::models::WebAuthnCredential;
|
||||||
pub use accounts::purge_auth_requests;
|
pub use accounts::purge_auth_requests;
|
||||||
pub use ciphers::{purge_trashed_ciphers, CipherData, CipherSyncData, CipherSyncType};
|
pub use ciphers::{purge_trashed_ciphers, CipherData, CipherSyncData, CipherSyncType};
|
||||||
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
|
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
|
||||||
|
@ -18,7 +21,7 @@ pub use sends::purge_sends;
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains];
|
let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains];
|
||||||
let mut hibp_routes = routes![hibp_breach];
|
let mut hibp_routes = routes![hibp_breach];
|
||||||
let mut meta_routes = routes![alive, now, version, config, get_api_webauthn];
|
let mut meta_routes = routes![alive, now, version, config, get_api_webauthn, post_api_webauthn, post_api_webauthn_attestation_options];
|
||||||
|
|
||||||
let mut routes = Vec::new();
|
let mut routes = Vec::new();
|
||||||
routes.append(&mut accounts::routes());
|
routes.append(&mut accounts::routes());
|
||||||
|
@ -48,15 +51,20 @@ pub fn events_routes() -> Vec<Route> {
|
||||||
// Move this somewhere else
|
// Move this somewhere else
|
||||||
//
|
//
|
||||||
use rocket::{serde::json::Json, serde::json::Value, Catcher, Route};
|
use rocket::{serde::json::Json, serde::json::Value, Catcher, Route};
|
||||||
|
use rocket::http::Status;
|
||||||
|
use webauthn_rs::proto::UserVerificationPolicy;
|
||||||
|
use webauthn_rs::RegistrationState;
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{JsonResult, Notify, UpdateType},
|
api::{JsonResult, Notify, UpdateType},
|
||||||
auth::Headers,
|
auth::Headers,
|
||||||
db::DbConn,
|
db::DbConn,
|
||||||
error::Error,
|
error::Error,
|
||||||
http_client::make_http_request,
|
http_client::make_http_request,
|
||||||
util::parse_experimental_client_feature_flags,
|
|
||||||
};
|
};
|
||||||
|
use crate::api::core::two_factor::webauthn::{RegisterPublicKeyCredentialCopy, WebauthnConfig};
|
||||||
|
use crate::api::{ApiResult, PasswordOrOtpData};
|
||||||
|
use crate::db::models::UserId;
|
||||||
|
use crate::util::parse_experimental_client_feature_flags;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -184,14 +192,143 @@ fn version() -> Json<&'static str> {
|
||||||
Json(crate::VERSION.unwrap_or_default())
|
Json(crate::VERSION.unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static WEBAUTHN_STATES: OnceLock<Mutex<HashMap<UserId, RegistrationState>>> = OnceLock::new();
|
||||||
|
|
||||||
|
#[post("/webauthn/attestation-options", data = "<data>")]
|
||||||
|
async fn post_api_webauthn_attestation_options(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
|
let data: PasswordOrOtpData = data.into_inner();
|
||||||
|
let user = headers.user;
|
||||||
|
|
||||||
|
// TODO what does delete_if_valid do?
|
||||||
|
data.validate(&user, false, &mut conn).await?;
|
||||||
|
|
||||||
|
// C# does this check as well
|
||||||
|
// await ValidateIfUserCanUsePasskeyLogin(user.Id);
|
||||||
|
|
||||||
|
// TODO add existing keys here when the table exists
|
||||||
|
// let registrations = get_webauthn_registrations(&user.uuid, &mut conn)
|
||||||
|
// .await?
|
||||||
|
// .1
|
||||||
|
// .into_iter()
|
||||||
|
// .map(|r| r.credential.cred_id) // We return the credentialIds to the clients to avoid double registering
|
||||||
|
// .collect();
|
||||||
|
|
||||||
|
let registrations = Vec::new();
|
||||||
|
|
||||||
|
let (challenge, state) = WebauthnConfig::load(true).generate_challenge_register_options(
|
||||||
|
user.uuid.as_bytes().to_vec(),
|
||||||
|
user.email,
|
||||||
|
user.name,
|
||||||
|
Some(registrations),
|
||||||
|
Some(UserVerificationPolicy::Required),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
WEBAUTHN_STATES.get_or_init(|| Mutex::new(HashMap::new())).lock().unwrap().insert(user.uuid, state);
|
||||||
|
|
||||||
|
let mut options = serde_json::to_value(challenge.public_key)?;
|
||||||
|
options["status"] = "ok".into();
|
||||||
|
options["errorMessage"] = "".into();
|
||||||
|
// TODO does this need to be set?
|
||||||
|
options["extensions"] = Value::Object(serde_json::Map::new());
|
||||||
|
|
||||||
|
// TODO make this nicer
|
||||||
|
let mut webauthn_credential_create_options = Value::Object(serde_json::Map::new());
|
||||||
|
webauthn_credential_create_options["options"] = options;
|
||||||
|
webauthn_credential_create_options["object"] = "webauthnCredentialCreateOptions".into();
|
||||||
|
|
||||||
|
// TODO this hopefully shouldn't be needed
|
||||||
|
// webauthn_credential_create_options["token"] = "atoken".into();
|
||||||
|
|
||||||
|
Ok(Json(webauthn_credential_create_options))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
// TODO remove allow dead_code
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct WebAuthnLoginCredentialCreateRequest {
|
||||||
|
device_response: RegisterPublicKeyCredentialCopy,
|
||||||
|
name: String,
|
||||||
|
// TODO this is hopefully not needed
|
||||||
|
// token: String,
|
||||||
|
supports_prf: bool,
|
||||||
|
encrypted_user_key: String,
|
||||||
|
encrypted_public_key: String,
|
||||||
|
encrypted_private_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/webauthn", data = "<data>")]
|
||||||
|
async fn post_api_webauthn(data: Json<WebAuthnLoginCredentialCreateRequest>, headers: Headers, mut conn: DbConn) -> ApiResult<Status> {
|
||||||
|
// this check await ValidateIfUserCanUsePasskeyLogin(user.Id); again
|
||||||
|
let data: WebAuthnLoginCredentialCreateRequest = data.into_inner();
|
||||||
|
// let data: WebAuthnLoginCredentialCreateRequest = serde_json::from_str(&data)?;
|
||||||
|
let user = headers.user;
|
||||||
|
|
||||||
|
// TODO Retrieve and delete the saved challenge state here
|
||||||
|
|
||||||
|
|
||||||
|
// Verify the credentials with the saved state
|
||||||
|
let (credential, _data) = {
|
||||||
|
let mut states = WEBAUTHN_STATES.get().unwrap().lock().unwrap();
|
||||||
|
let state = states.remove(&user.uuid).unwrap();
|
||||||
|
|
||||||
|
WebauthnConfig::load(true).register_credential(&data.device_response.into(), &state, |_| Ok(false))?
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO add existing keys here when the table exists
|
||||||
|
// let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
|
||||||
|
// // TODO: Check for repeated ID's
|
||||||
|
// registrations.push(WebauthnRegistration {
|
||||||
|
// id: data.id.into_i32()?,
|
||||||
|
// name: data.name,
|
||||||
|
// migrated: false,
|
||||||
|
//
|
||||||
|
// credential,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// let registrations = Vec::new();
|
||||||
|
|
||||||
|
// TODO Save the registration
|
||||||
|
// TwoFactor::new(user.uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?)
|
||||||
|
// .save(&mut conn)
|
||||||
|
// .await?;
|
||||||
|
|
||||||
|
WebAuthnCredential::new(
|
||||||
|
user.uuid,
|
||||||
|
data.name,
|
||||||
|
serde_json::to_string(&credential)?,
|
||||||
|
data.supports_prf,
|
||||||
|
data.encrypted_user_key,
|
||||||
|
data.encrypted_public_key,
|
||||||
|
data.encrypted_private_key,
|
||||||
|
).save(&mut conn).await?;
|
||||||
|
|
||||||
|
Ok(Status::Ok)
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/webauthn")]
|
#[get("/webauthn")]
|
||||||
fn get_api_webauthn(_headers: Headers) -> Json<Value> {
|
async fn get_api_webauthn(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
||||||
// Prevent a 404 error, which also causes key-rotation issues
|
let user = headers.user;
|
||||||
// It looks like this is used when login with passkeys is enabled, which Vaultwarden does not (yet) support
|
|
||||||
// An empty list/data also works fine
|
let data = WebAuthnCredential::find_all_by_user(&user.uuid, &mut conn)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(|wac| {
|
||||||
|
// TODO generate prfStatus from GetPrfStatus() in C#
|
||||||
|
json!({
|
||||||
|
"id": wac.uuid,
|
||||||
|
"name": wac.name,
|
||||||
|
"prfStatus": 0,
|
||||||
|
"encryptedUserKey": wac.encrypted_user_key,
|
||||||
|
"encryptedPublicKey": wac.encrypted_public_key,
|
||||||
|
"object": "webauthnCredential",
|
||||||
|
})
|
||||||
|
}).collect::<Value>();
|
||||||
|
|
||||||
Json(json!({
|
Json(json!({
|
||||||
"object": "list",
|
"object": "list",
|
||||||
"data": [],
|
"data": data,
|
||||||
"continuationToken": null
|
"continuationToken": null
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,20 +45,22 @@ pub struct U2FRegistration {
|
||||||
pub migrated: Option<bool>,
|
pub migrated: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WebauthnConfig {
|
pub(crate) struct WebauthnConfig {
|
||||||
url: String,
|
url: String,
|
||||||
origin: Url,
|
origin: Url,
|
||||||
rpid: String,
|
rpid: String,
|
||||||
|
require_resident_key: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebauthnConfig {
|
impl WebauthnConfig {
|
||||||
fn load() -> Webauthn<Self> {
|
pub(crate) fn load(require_resident_key: bool) -> Webauthn<Self> {
|
||||||
let domain = CONFIG.domain();
|
let domain = CONFIG.domain();
|
||||||
let domain_origin = CONFIG.domain_origin();
|
let domain_origin = CONFIG.domain_origin();
|
||||||
Webauthn::new(Self {
|
Webauthn::new(Self {
|
||||||
rpid: Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(),
|
rpid: Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(),
|
||||||
url: domain,
|
url: domain,
|
||||||
origin: Url::parse(&domain_origin).unwrap(),
|
origin: Url::parse(&domain_origin).unwrap(),
|
||||||
|
require_resident_key,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,6 +84,26 @@ impl webauthn_rs::WebauthnConfig for WebauthnConfig {
|
||||||
fn get_require_uv_consistency(&self) -> bool {
|
fn get_require_uv_consistency(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_require_resident_key(&self) -> bool {
|
||||||
|
self.require_resident_key
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO check if this still works with 2FA
|
||||||
|
fn get_credential_algorithms(&self) -> Vec<COSEAlgorithm> {
|
||||||
|
vec![
|
||||||
|
COSEAlgorithm::ES256,
|
||||||
|
COSEAlgorithm::RS256,
|
||||||
|
COSEAlgorithm::PS256,
|
||||||
|
COSEAlgorithm::ES384,
|
||||||
|
COSEAlgorithm::RS384,
|
||||||
|
COSEAlgorithm::PS384,
|
||||||
|
COSEAlgorithm::ES512,
|
||||||
|
COSEAlgorithm::RS512,
|
||||||
|
COSEAlgorithm::PS512,
|
||||||
|
COSEAlgorithm::EDDSA,
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
@ -124,6 +146,7 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn:
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Creation call
|
||||||
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
|
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
|
||||||
async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let data: PasswordOrOtpData = data.into_inner();
|
let data: PasswordOrOtpData = data.into_inner();
|
||||||
|
@ -138,7 +161,7 @@ async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Hea
|
||||||
.map(|r| r.credential.cred_id) // We return the credentialIds to the clients to avoid double registering
|
.map(|r| r.credential.cred_id) // We return the credentialIds to the clients to avoid double registering
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let (challenge, state) = WebauthnConfig::load().generate_challenge_register_options(
|
let (challenge, state) = WebauthnConfig::load(false).generate_challenge_register_options(
|
||||||
user.uuid.as_bytes().to_vec(),
|
user.uuid.as_bytes().to_vec(),
|
||||||
user.email,
|
user.email,
|
||||||
user.name,
|
user.name,
|
||||||
|
@ -168,11 +191,12 @@ struct EnableWebauthnData {
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct RegisterPublicKeyCredentialCopy {
|
pub struct RegisterPublicKeyCredentialCopy {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub raw_id: Base64UrlSafeData,
|
pub raw_id: Base64UrlSafeData,
|
||||||
pub response: AuthenticatorAttestationResponseRawCopy,
|
pub response: AuthenticatorAttestationResponseRawCopy,
|
||||||
pub r#type: String,
|
pub r#type: String,
|
||||||
|
pub extensions: Option<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is copied from AuthenticatorAttestationResponseRaw to change clientDataJSON to clientDataJson
|
// This is copied from AuthenticatorAttestationResponseRaw to change clientDataJSON to clientDataJson
|
||||||
|
@ -237,6 +261,7 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Confirmation call
|
||||||
#[post("/two-factor/webauthn", data = "<data>")]
|
#[post("/two-factor/webauthn", data = "<data>")]
|
||||||
async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let data: EnableWebauthnData = data.into_inner();
|
let data: EnableWebauthnData = data.into_inner();
|
||||||
|
@ -262,7 +287,7 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut
|
||||||
|
|
||||||
// Verify the credentials with the saved state
|
// Verify the credentials with the saved state
|
||||||
let (credential, _data) =
|
let (credential, _data) =
|
||||||
WebauthnConfig::load().register_credential(&data.device_response.into(), &state, |_| Ok(false))?;
|
WebauthnConfig::load(false).register_credential(&data.device_response.into(), &state, |_| Ok(false))?;
|
||||||
|
|
||||||
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
|
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
|
||||||
// TODO: Check for repeated ID's
|
// TODO: Check for repeated ID's
|
||||||
|
@ -373,7 +398,7 @@ pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> Jso
|
||||||
|
|
||||||
// Generate a challenge based on the credentials
|
// Generate a challenge based on the credentials
|
||||||
let ext = RequestAuthenticationExtensions::builder().appid(format!("{}/app-id.json", &CONFIG.domain())).build();
|
let ext = RequestAuthenticationExtensions::builder().appid(format!("{}/app-id.json", &CONFIG.domain())).build();
|
||||||
let (response, state) = WebauthnConfig::load().generate_challenge_authenticate_options(creds, Some(ext))?;
|
let (response, state) = WebauthnConfig::load(false).generate_challenge_authenticate_options(creds, Some(ext))?;
|
||||||
|
|
||||||
// Save the challenge state for later validation
|
// Save the challenge state for later validation
|
||||||
TwoFactor::new(user_id.clone(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?)
|
TwoFactor::new(user_id.clone(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?)
|
||||||
|
@ -407,7 +432,7 @@ pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mu
|
||||||
|
|
||||||
// If the credential we received is migrated from U2F, enable the U2F compatibility
|
// 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 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)?;
|
let (cred_id, auth_data) = WebauthnConfig::load(false).authenticate_credential(&rsp, &state)?;
|
||||||
|
|
||||||
for reg in &mut registrations {
|
for reg in &mut registrations {
|
||||||
if ®.credential.cred_id == cred_id {
|
if ®.credential.cred_id == cred_id {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
|
@ -6,7 +8,9 @@ use rocket::{
|
||||||
Route,
|
Route,
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use webauthn_rs::AuthenticationState;
|
||||||
|
use webauthn_rs::base64_data::Base64UrlSafeData;
|
||||||
|
use webauthn_rs::proto::{AuthenticatorAssertionResponseRaw, Credential, PublicKeyCredential};
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
core::{
|
core::{
|
||||||
|
@ -23,9 +27,10 @@ use crate::{
|
||||||
error::MapResult,
|
error::MapResult,
|
||||||
mail, util, CONFIG,
|
mail, util, CONFIG,
|
||||||
};
|
};
|
||||||
|
use crate::api::core::two_factor::webauthn::WebauthnConfig;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![login, prelogin, identity_register, register_verification_email, register_finish]
|
routes![login, prelogin, identity_register, register_verification_email, register_finish, get_web_authn_assertion_options]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/connect/token", data = "<data>")]
|
#[post("/connect/token", data = "<data>")]
|
||||||
|
@ -66,7 +71,20 @@ async fn login(
|
||||||
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
_api_key_login(data, &mut user_id, &mut conn, &client_header.ip).await
|
_api_key_login(data, &mut user_id, &mut conn, &client_header.ip).await
|
||||||
}
|
},
|
||||||
|
"webauthn" => {
|
||||||
|
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
||||||
|
_check_is_some(&data.scope, "scope cannot be blank")?;
|
||||||
|
|
||||||
|
_check_is_some(&data.device_identifier, "device_identifier cannot be blank")?;
|
||||||
|
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||||
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
|
_check_is_some(&data.device_response, "device_response cannot be blank")?;
|
||||||
|
_check_is_some(&data.token, "token cannot be blank")?;
|
||||||
|
|
||||||
|
_webauthn_login(data, &mut user_id, &mut conn, &client_header.ip).await
|
||||||
|
},
|
||||||
t => err!("Invalid type", t),
|
t => err!("Invalid type", t),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -100,6 +118,254 @@ async fn login(
|
||||||
login_result
|
login_result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PublicKeyCredentialCopy {
|
||||||
|
pub id: String,
|
||||||
|
pub raw_id: Base64UrlSafeData,
|
||||||
|
pub response: AuthenticatorAssertionResponseRawCopy,
|
||||||
|
pub r#type: String,
|
||||||
|
// TODO think about what to do with this field, currently this is ignored in the conversion
|
||||||
|
pub extensions: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is copied from AuthenticatorAssertionResponseRaw to change clientDataJSON to clientDataJson
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AuthenticatorAssertionResponseRawCopy {
|
||||||
|
pub authenticator_data: Base64UrlSafeData,
|
||||||
|
#[serde(rename = "clientDataJson", alias = "clientDataJSON")]
|
||||||
|
pub client_data_json: Base64UrlSafeData,
|
||||||
|
pub signature: Base64UrlSafeData,
|
||||||
|
pub user_handle: Option<Base64UrlSafeData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
|
||||||
|
fn from(p: PublicKeyCredentialCopy) -> Self {
|
||||||
|
Self {
|
||||||
|
id: p.id,
|
||||||
|
raw_id: p.raw_id,
|
||||||
|
response: AuthenticatorAssertionResponseRaw {
|
||||||
|
authenticator_data: p.response.authenticator_data,
|
||||||
|
client_data_json: p.response.client_data_json,
|
||||||
|
signature: p.response.signature,
|
||||||
|
user_handle: p.response.user_handle,
|
||||||
|
},
|
||||||
|
extensions: None,
|
||||||
|
type_: p.r#type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn _webauthn_login(
|
||||||
|
data: ConnectData,
|
||||||
|
user_id: &mut Option<UserId>,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
ip: &ClientIp,
|
||||||
|
) -> JsonResult {
|
||||||
|
// Validate scope
|
||||||
|
let scope = data.scope.as_ref().unwrap();
|
||||||
|
if scope != "api offline_access" {
|
||||||
|
err!("Scope not supported")
|
||||||
|
}
|
||||||
|
let scope_vec = vec!["api".into(), "offline_access".into()];
|
||||||
|
|
||||||
|
// Ratelimit the login
|
||||||
|
crate::ratelimit::check_limit_login(&ip.ip)?;
|
||||||
|
|
||||||
|
let device_response: PublicKeyCredentialCopy = serde_json::from_str(&data.device_response.as_ref().unwrap())?;
|
||||||
|
let mut user = if let Some(uuid) = device_response.response.user_handle.clone() {
|
||||||
|
// TODO handle error
|
||||||
|
let uuid = UserId(String::from_utf8(uuid.0).unwrap());
|
||||||
|
User::find_by_uuid(&uuid, conn).await.unwrap()
|
||||||
|
} else {
|
||||||
|
err!("DeviceResponse needs the userHandle field")
|
||||||
|
};
|
||||||
|
let username = user.name.clone();
|
||||||
|
|
||||||
|
// Set the user_id here to be passed back used for event logging.
|
||||||
|
*user_id = Some(user.uuid.clone());
|
||||||
|
|
||||||
|
// Check if the user is disabled
|
||||||
|
if !user.enabled {
|
||||||
|
err!(
|
||||||
|
"This user has been disabled",
|
||||||
|
format!("IP: {}. Username: {username}.", ip.ip),
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let web_authn_credentials = WebAuthnCredential::find_all_by_user(&user.uuid, conn).await;
|
||||||
|
|
||||||
|
let credentials = web_authn_credentials
|
||||||
|
.iter()
|
||||||
|
.map(|c| {
|
||||||
|
serde_json::from_str(&c.credential)
|
||||||
|
}).collect::<Result<Vec<Credential>, _>>()?;
|
||||||
|
|
||||||
|
let web_authn_credential = {
|
||||||
|
let token = data.token.as_ref().unwrap();
|
||||||
|
let mut states = WEBAUTHN_AUTHENTICATION_STATES.get().unwrap().lock().unwrap();
|
||||||
|
let mut state = states.remove(token).unwrap();
|
||||||
|
let resp = device_response.into();
|
||||||
|
|
||||||
|
state.set_allowed_credentials(credentials);
|
||||||
|
|
||||||
|
// TODO update respective credential in database
|
||||||
|
let (credential_id, auth_data) = WebauthnConfig::load(true)
|
||||||
|
.authenticate_credential(&resp, &state)?;
|
||||||
|
|
||||||
|
if !auth_data.user_verified {
|
||||||
|
// TODO throw an error here
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
|
||||||
|
web_authn_credentials.into_iter()
|
||||||
|
.find(|c| &serde_json::from_str::<Credential>(&c.credential).unwrap().cred_id == credential_id)
|
||||||
|
.unwrap()
|
||||||
|
|
||||||
|
/* TODO return this error on failure
|
||||||
|
err!(
|
||||||
|
"Username or password is incorrect. Try again",
|
||||||
|
format!("IP: {}. Username: {username}.", ip.ip),
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
|
||||||
|
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
|
||||||
|
if user.last_verifying_at.is_none()
|
||||||
|
|| now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds()
|
||||||
|
> CONFIG.signups_verify_resend_time() as i64
|
||||||
|
{
|
||||||
|
let resend_limit = CONFIG.signups_verify_resend_limit() as i32;
|
||||||
|
if resend_limit == 0 || user.login_verify_count < resend_limit {
|
||||||
|
// We want to send another email verification if we require signups to verify
|
||||||
|
// their email address, and we haven't sent them a reminder in a while...
|
||||||
|
user.last_verifying_at = Some(now);
|
||||||
|
user.login_verify_count += 1;
|
||||||
|
|
||||||
|
if let Err(e) = user.save(conn).await {
|
||||||
|
error!("Error updating user: {e:#?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = mail::send_verify_email(&user.email, &user.uuid).await {
|
||||||
|
error!("Error auto-sending email verification email: {e:#?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We still want the login to fail until they actually verified the email address
|
||||||
|
err!(
|
||||||
|
"Please verify your email before trying again.",
|
||||||
|
format!("IP: {}. Username: {username}.", ip.ip),
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut device, new_device) = get_device(&data, conn, &user).await;
|
||||||
|
|
||||||
|
// TODO is this needed with passkeys?
|
||||||
|
if CONFIG.mail_enabled() && new_device {
|
||||||
|
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await {
|
||||||
|
error!("Error sending new device email: {e:#?}");
|
||||||
|
|
||||||
|
if CONFIG.require_device_email() {
|
||||||
|
err!(
|
||||||
|
"Could not send login notification email. Please contact your administrator.",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// register push device
|
||||||
|
if !new_device {
|
||||||
|
register_push_device(&mut device, conn).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common
|
||||||
|
// ---
|
||||||
|
// Disabled this variable, it was used to generate the JWT
|
||||||
|
// Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out
|
||||||
|
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
|
||||||
|
// ---
|
||||||
|
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
|
||||||
|
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec, data.client_id);
|
||||||
|
device.save(conn).await?;
|
||||||
|
|
||||||
|
// Fetch all valid Master Password Policies and merge them into one with all trues and largest numbers as one policy
|
||||||
|
let master_password_policies: Vec<MasterPasswordPolicy> =
|
||||||
|
OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(
|
||||||
|
&user.uuid,
|
||||||
|
OrgPolicyType::MasterPassword,
|
||||||
|
conn,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|p| serde_json::from_str(&p.data).ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// NOTE: Upstream still uses PascalCase here for `Object`!
|
||||||
|
let master_password_policy = if !master_password_policies.is_empty() {
|
||||||
|
let mut mpp_json = json!(master_password_policies.into_iter().reduce(|acc, policy| {
|
||||||
|
MasterPasswordPolicy {
|
||||||
|
min_complexity: acc.min_complexity.max(policy.min_complexity),
|
||||||
|
min_length: acc.min_length.max(policy.min_length),
|
||||||
|
require_lower: acc.require_lower || policy.require_lower,
|
||||||
|
require_upper: acc.require_upper || policy.require_upper,
|
||||||
|
require_numbers: acc.require_numbers || policy.require_numbers,
|
||||||
|
require_special: acc.require_special || policy.require_special,
|
||||||
|
enforce_on_login: acc.enforce_on_login || policy.enforce_on_login,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
mpp_json["Object"] = json!("masterPasswordPolicy");
|
||||||
|
mpp_json
|
||||||
|
} else {
|
||||||
|
json!({"Object": "masterPasswordPolicy"})
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = json!({
|
||||||
|
"access_token": access_token,
|
||||||
|
"expires_in": expires_in,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"refresh_token": device.refresh_token,
|
||||||
|
"Key": user.akey,
|
||||||
|
"PrivateKey": user.private_key,
|
||||||
|
|
||||||
|
"Kdf": user.client_kdf_type,
|
||||||
|
"KdfIterations": user.client_kdf_iter,
|
||||||
|
"KdfMemory": user.client_kdf_memory,
|
||||||
|
"KdfParallelism": user.client_kdf_parallelism,
|
||||||
|
"ResetMasterPassword": false, // TODO: Same as above
|
||||||
|
"ForcePasswordReset": false,
|
||||||
|
"MasterPasswordPolicy": master_password_policy,
|
||||||
|
|
||||||
|
"scope": scope,
|
||||||
|
"UserDecryptionOptions": {
|
||||||
|
"HasMasterPassword": !user.password_hash.is_empty(),
|
||||||
|
"WebAuthnPrfOption": {
|
||||||
|
"EncryptedPrivateKey": web_authn_credential.encrypted_private_key,
|
||||||
|
"EncryptedUserKey": web_authn_credential.encrypted_user_key,
|
||||||
|
},
|
||||||
|
"Object": "userDecryptionOptions"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
info!("User {username} logged in successfully. IP: {}", ip.ip);
|
||||||
|
Ok(Json(result))
|
||||||
|
}
|
||||||
|
|
||||||
async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
|
async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
|
||||||
// Extract token
|
// Extract token
|
||||||
let token = data.refresh_token.unwrap();
|
let token = data.refresh_token.unwrap();
|
||||||
|
@ -697,6 +963,30 @@ async fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult
|
||||||
_register(data, false, conn).await
|
_register(data, false, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static WEBAUTHN_AUTHENTICATION_STATES: OnceLock<Mutex<HashMap<String, AuthenticationState>>> = OnceLock::new();
|
||||||
|
|
||||||
|
#[get("/accounts/webauthn/assertion-options")]
|
||||||
|
fn get_web_authn_assertion_options() -> JsonResult {
|
||||||
|
let (options, state) = WebauthnConfig::load(true)
|
||||||
|
.generate_challenge_authenticate_options(
|
||||||
|
Vec::new(),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// TODO this needs to be solved in another way to avoid DoS
|
||||||
|
let t = util::get_uuid();
|
||||||
|
WEBAUTHN_AUTHENTICATION_STATES.get_or_init(|| Mutex::new(HashMap::new())).lock().unwrap().insert(t.clone(), state);
|
||||||
|
|
||||||
|
let options = serde_json::to_value(options.public_key)?;
|
||||||
|
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"options": options,
|
||||||
|
"token": t,
|
||||||
|
"object": "webAuthnLoginAssertionOptions"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct RegisterVerificationData {
|
struct RegisterVerificationData {
|
||||||
|
@ -809,6 +1099,13 @@ struct ConnectData {
|
||||||
two_factor_remember: Option<i32>,
|
two_factor_remember: Option<i32>,
|
||||||
#[field(name = uncased("authrequest"))]
|
#[field(name = uncased("authrequest"))]
|
||||||
auth_request: Option<AuthRequestId>,
|
auth_request: Option<AuthRequestId>,
|
||||||
|
|
||||||
|
// Needed for "login with passkey"
|
||||||
|
#[field(name = uncased("deviceresponse"))]
|
||||||
|
device_response: Option<String>,
|
||||||
|
// TODO this may be removed again if implemented correctly
|
||||||
|
#[field(name = uncased("token"))]
|
||||||
|
token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
|
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
|
||||||
|
|
|
@ -15,6 +15,7 @@ mod two_factor;
|
||||||
mod two_factor_duo_context;
|
mod two_factor_duo_context;
|
||||||
mod two_factor_incomplete;
|
mod two_factor_incomplete;
|
||||||
mod user;
|
mod user;
|
||||||
|
mod web_authn_credential;
|
||||||
|
|
||||||
pub use self::attachment::{Attachment, AttachmentId};
|
pub use self::attachment::{Attachment, AttachmentId};
|
||||||
pub use self::auth_request::{AuthRequest, AuthRequestId};
|
pub use self::auth_request::{AuthRequest, AuthRequestId};
|
||||||
|
@ -39,3 +40,4 @@ pub use self::two_factor::{TwoFactor, TwoFactorType};
|
||||||
pub use self::two_factor_duo_context::TwoFactorDuoContext;
|
pub use self::two_factor_duo_context::TwoFactorDuoContext;
|
||||||
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
||||||
pub use self::user::{Invitation, User, UserId, UserKdfType, UserStampException};
|
pub use self::user::{Invitation, User, UserId, UserKdfType, UserStampException};
|
||||||
|
pub use self::web_authn_credential::WebAuthnCredential;
|
||||||
|
|
|
@ -476,4 +476,5 @@ impl Invitation {
|
||||||
)]
|
)]
|
||||||
#[deref(forward)]
|
#[deref(forward)]
|
||||||
#[from(forward)]
|
#[from(forward)]
|
||||||
pub struct UserId(String);
|
// TODO create a way to construct this
|
||||||
|
pub struct UserId(pub String);
|
||||||
|
|
89
src/db/models/web_authn_credential.rs
Normal file
89
src/db/models/web_authn_credential.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
use derive_more::{AsRef, Deref, Display, From};
|
||||||
|
use macros::UuidFromParam;
|
||||||
|
use crate::api::EmptyResult;
|
||||||
|
use crate::db::DbConn;
|
||||||
|
use super::UserId;
|
||||||
|
|
||||||
|
db_object! {
|
||||||
|
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)]
|
||||||
|
#[diesel(table_name = web_authn_credentials)]
|
||||||
|
#[diesel(treat_none_as_null = true)]
|
||||||
|
#[diesel(primary_key(uuid))]
|
||||||
|
pub struct WebAuthnCredential {
|
||||||
|
pub uuid: WebAuthnCredentialId,
|
||||||
|
pub user_uuid: UserId,
|
||||||
|
pub name: String,
|
||||||
|
pub credential: String,
|
||||||
|
pub supports_prf: bool,
|
||||||
|
pub encrypted_user_key: String,
|
||||||
|
pub encrypted_public_key: String,
|
||||||
|
pub encrypted_private_key: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebAuthnCredential {
|
||||||
|
pub fn new(
|
||||||
|
user_uuid: UserId,
|
||||||
|
name: String,
|
||||||
|
credential: String,
|
||||||
|
supports_prf: bool,
|
||||||
|
encrypted_user_key: String,
|
||||||
|
encrypted_public_key: String,
|
||||||
|
encrypted_private_key: String,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
uuid: WebAuthnCredentialId(crate::util::get_uuid()),
|
||||||
|
user_uuid,
|
||||||
|
name,
|
||||||
|
credential,
|
||||||
|
supports_prf,
|
||||||
|
encrypted_user_key,
|
||||||
|
encrypted_public_key,
|
||||||
|
encrypted_private_key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
|
||||||
|
// TODO add mysql and postgres
|
||||||
|
db_run! { conn:
|
||||||
|
sqlite {
|
||||||
|
match diesel::insert_into(web_authn_credentials::table)
|
||||||
|
.values(WebAuthnCredentialDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||||
|
db_run! { conn: {
|
||||||
|
web_authn_credentials::table
|
||||||
|
.filter(web_authn_credentials::user_uuid.eq(user_uuid))
|
||||||
|
.load::<WebAuthnCredentialDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
// TODO do not unwrap
|
||||||
|
}}.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone,
|
||||||
|
Debug,
|
||||||
|
AsRef,
|
||||||
|
Deref,
|
||||||
|
DieselNewType,
|
||||||
|
Display,
|
||||||
|
From,
|
||||||
|
FromForm,
|
||||||
|
Hash,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
UuidFromParam,
|
||||||
|
)]
|
||||||
|
pub struct WebAuthnCredentialId(String);
|
398
src/db/schema.rs
Normal file
398
src/db/schema.rs
Normal file
|
@ -0,0 +1,398 @@
|
||||||
|
// @generated automatically by Diesel CLI.
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
attachments (id) {
|
||||||
|
id -> Text,
|
||||||
|
cipher_uuid -> Text,
|
||||||
|
file_name -> Text,
|
||||||
|
file_size -> Integer,
|
||||||
|
akey -> Nullable<Text>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
auth_requests (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Text,
|
||||||
|
organization_uuid -> Nullable<Text>,
|
||||||
|
request_device_identifier -> Text,
|
||||||
|
device_type -> Integer,
|
||||||
|
request_ip -> Text,
|
||||||
|
response_device_id -> Nullable<Text>,
|
||||||
|
access_code -> Text,
|
||||||
|
public_key -> Text,
|
||||||
|
enc_key -> Nullable<Text>,
|
||||||
|
master_password_hash -> Nullable<Text>,
|
||||||
|
approved -> Nullable<Bool>,
|
||||||
|
creation_date -> Timestamp,
|
||||||
|
response_date -> Nullable<Timestamp>,
|
||||||
|
authentication_date -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
ciphers (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
user_uuid -> Nullable<Text>,
|
||||||
|
organization_uuid -> Nullable<Text>,
|
||||||
|
atype -> Integer,
|
||||||
|
name -> Text,
|
||||||
|
notes -> Nullable<Text>,
|
||||||
|
fields -> Nullable<Text>,
|
||||||
|
data -> Text,
|
||||||
|
password_history -> Nullable<Text>,
|
||||||
|
deleted_at -> Nullable<Timestamp>,
|
||||||
|
reprompt -> Nullable<Integer>,
|
||||||
|
key -> Nullable<Text>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
ciphers_collections (cipher_uuid, collection_uuid) {
|
||||||
|
cipher_uuid -> Text,
|
||||||
|
collection_uuid -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
collections (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
org_uuid -> Text,
|
||||||
|
name -> Text,
|
||||||
|
external_id -> Nullable<Text>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
collections_groups (rowid) {
|
||||||
|
rowid -> Integer,
|
||||||
|
collections_uuid -> Text,
|
||||||
|
groups_uuid -> Text,
|
||||||
|
read_only -> Bool,
|
||||||
|
hide_passwords -> Bool,
|
||||||
|
manage -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
devices (uuid, user_uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
user_uuid -> Text,
|
||||||
|
name -> Text,
|
||||||
|
atype -> Integer,
|
||||||
|
push_token -> Nullable<Text>,
|
||||||
|
refresh_token -> Text,
|
||||||
|
twofactor_remember -> Nullable<Text>,
|
||||||
|
push_uuid -> Nullable<Text>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
emergency_access (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
grantor_uuid -> Nullable<Text>,
|
||||||
|
grantee_uuid -> Nullable<Text>,
|
||||||
|
email -> Nullable<Text>,
|
||||||
|
key_encrypted -> Nullable<Text>,
|
||||||
|
atype -> Integer,
|
||||||
|
status -> Integer,
|
||||||
|
wait_time_days -> Integer,
|
||||||
|
recovery_initiated_at -> Nullable<Timestamp>,
|
||||||
|
last_notification_at -> Nullable<Timestamp>,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
event (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
event_type -> Integer,
|
||||||
|
user_uuid -> Nullable<Text>,
|
||||||
|
org_uuid -> Nullable<Text>,
|
||||||
|
cipher_uuid -> Nullable<Text>,
|
||||||
|
collection_uuid -> Nullable<Text>,
|
||||||
|
group_uuid -> Nullable<Text>,
|
||||||
|
org_user_uuid -> Nullable<Text>,
|
||||||
|
act_user_uuid -> Nullable<Text>,
|
||||||
|
device_type -> Nullable<Integer>,
|
||||||
|
ip_address -> Nullable<Text>,
|
||||||
|
event_date -> Timestamp,
|
||||||
|
policy_uuid -> Nullable<Text>,
|
||||||
|
provider_uuid -> Nullable<Text>,
|
||||||
|
provider_user_uuid -> Nullable<Text>,
|
||||||
|
provider_org_uuid -> Nullable<Text>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
favorites (user_uuid, cipher_uuid) {
|
||||||
|
user_uuid -> Text,
|
||||||
|
cipher_uuid -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
folders (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
user_uuid -> Text,
|
||||||
|
name -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
folders_ciphers (cipher_uuid, folder_uuid) {
|
||||||
|
cipher_uuid -> Text,
|
||||||
|
folder_uuid -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
groups (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
organizations_uuid -> Text,
|
||||||
|
name -> Text,
|
||||||
|
access_all -> Bool,
|
||||||
|
external_id -> Nullable<Text>,
|
||||||
|
creation_date -> Timestamp,
|
||||||
|
revision_date -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
groups_users (rowid) {
|
||||||
|
rowid -> Integer,
|
||||||
|
groups_uuid -> Text,
|
||||||
|
users_organizations_uuid -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
invitations (email) {
|
||||||
|
email -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
org_policies (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
org_uuid -> Text,
|
||||||
|
atype -> Integer,
|
||||||
|
enabled -> Bool,
|
||||||
|
data -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
organization_api_key (uuid, org_uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
org_uuid -> Text,
|
||||||
|
atype -> Integer,
|
||||||
|
api_key -> Text,
|
||||||
|
revision_date -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
organizations (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
name -> Text,
|
||||||
|
billing_email -> Text,
|
||||||
|
private_key -> Nullable<Text>,
|
||||||
|
public_key -> Nullable<Text>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
sends (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Nullable<Text>,
|
||||||
|
organization_uuid -> Nullable<Text>,
|
||||||
|
name -> Text,
|
||||||
|
notes -> Nullable<Text>,
|
||||||
|
atype -> Integer,
|
||||||
|
data -> Text,
|
||||||
|
akey -> Text,
|
||||||
|
password_hash -> Nullable<Binary>,
|
||||||
|
password_salt -> Nullable<Binary>,
|
||||||
|
password_iter -> Nullable<Integer>,
|
||||||
|
max_access_count -> Nullable<Integer>,
|
||||||
|
access_count -> Integer,
|
||||||
|
creation_date -> Timestamp,
|
||||||
|
revision_date -> Timestamp,
|
||||||
|
expiration_date -> Nullable<Timestamp>,
|
||||||
|
deletion_date -> Timestamp,
|
||||||
|
disabled -> Bool,
|
||||||
|
hide_email -> Nullable<Bool>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
twofactor (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Text,
|
||||||
|
atype -> Integer,
|
||||||
|
enabled -> Bool,
|
||||||
|
data -> Text,
|
||||||
|
last_used -> Integer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
twofactor_duo_ctx (state) {
|
||||||
|
state -> Text,
|
||||||
|
user_email -> Text,
|
||||||
|
nonce -> Text,
|
||||||
|
exp -> Integer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
twofactor_incomplete (user_uuid, device_uuid) {
|
||||||
|
user_uuid -> Text,
|
||||||
|
device_uuid -> Text,
|
||||||
|
device_name -> Text,
|
||||||
|
login_time -> Timestamp,
|
||||||
|
ip_address -> Text,
|
||||||
|
device_type -> Integer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
users (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
email -> Text,
|
||||||
|
name -> Text,
|
||||||
|
password_hash -> Binary,
|
||||||
|
salt -> Binary,
|
||||||
|
password_iterations -> Integer,
|
||||||
|
password_hint -> Nullable<Text>,
|
||||||
|
akey -> Text,
|
||||||
|
private_key -> Nullable<Text>,
|
||||||
|
public_key -> Nullable<Text>,
|
||||||
|
totp_secret -> Nullable<Text>,
|
||||||
|
totp_recover -> Nullable<Text>,
|
||||||
|
security_stamp -> Text,
|
||||||
|
equivalent_domains -> Text,
|
||||||
|
excluded_globals -> Text,
|
||||||
|
client_kdf_type -> Integer,
|
||||||
|
client_kdf_iter -> Integer,
|
||||||
|
verified_at -> Nullable<Timestamp>,
|
||||||
|
last_verifying_at -> Nullable<Timestamp>,
|
||||||
|
login_verify_count -> Integer,
|
||||||
|
email_new -> Nullable<Text>,
|
||||||
|
email_new_token -> Nullable<Text>,
|
||||||
|
enabled -> Bool,
|
||||||
|
stamp_exception -> Nullable<Text>,
|
||||||
|
api_key -> Nullable<Text>,
|
||||||
|
avatar_color -> Nullable<Text>,
|
||||||
|
client_kdf_memory -> Nullable<Integer>,
|
||||||
|
client_kdf_parallelism -> Nullable<Integer>,
|
||||||
|
external_id -> Nullable<Text>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
users_collections (user_uuid, collection_uuid) {
|
||||||
|
user_uuid -> Text,
|
||||||
|
collection_uuid -> Text,
|
||||||
|
read_only -> Bool,
|
||||||
|
hide_passwords -> Bool,
|
||||||
|
manage -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
users_organizations (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Text,
|
||||||
|
org_uuid -> Text,
|
||||||
|
access_all -> Bool,
|
||||||
|
akey -> Text,
|
||||||
|
status -> Integer,
|
||||||
|
atype -> Integer,
|
||||||
|
reset_password_key -> Nullable<Text>,
|
||||||
|
external_id -> Nullable<Text>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
web_authn_credentials (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Text,
|
||||||
|
name -> Text,
|
||||||
|
credential -> Text,
|
||||||
|
supports_prf -> Bool,
|
||||||
|
encrypted_user_key -> Text,
|
||||||
|
encrypted_public_key -> Text,
|
||||||
|
encrypted_private_key -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::joinable!(attachments -> ciphers (cipher_uuid));
|
||||||
|
diesel::joinable!(auth_requests -> organizations (organization_uuid));
|
||||||
|
diesel::joinable!(auth_requests -> users (user_uuid));
|
||||||
|
diesel::joinable!(ciphers -> organizations (organization_uuid));
|
||||||
|
diesel::joinable!(ciphers -> users (user_uuid));
|
||||||
|
diesel::joinable!(ciphers_collections -> ciphers (cipher_uuid));
|
||||||
|
diesel::joinable!(ciphers_collections -> collections (collection_uuid));
|
||||||
|
diesel::joinable!(collections -> organizations (org_uuid));
|
||||||
|
diesel::joinable!(collections_groups -> collections (collections_uuid));
|
||||||
|
diesel::joinable!(collections_groups -> groups (groups_uuid));
|
||||||
|
diesel::joinable!(devices -> users (user_uuid));
|
||||||
|
diesel::joinable!(favorites -> ciphers (cipher_uuid));
|
||||||
|
diesel::joinable!(favorites -> users (user_uuid));
|
||||||
|
diesel::joinable!(folders -> users (user_uuid));
|
||||||
|
diesel::joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
||||||
|
diesel::joinable!(folders_ciphers -> folders (folder_uuid));
|
||||||
|
diesel::joinable!(groups -> organizations (organizations_uuid));
|
||||||
|
diesel::joinable!(groups_users -> groups (groups_uuid));
|
||||||
|
diesel::joinable!(groups_users -> users_organizations (users_organizations_uuid));
|
||||||
|
diesel::joinable!(org_policies -> organizations (org_uuid));
|
||||||
|
diesel::joinable!(organization_api_key -> organizations (org_uuid));
|
||||||
|
diesel::joinable!(sends -> organizations (organization_uuid));
|
||||||
|
diesel::joinable!(sends -> users (user_uuid));
|
||||||
|
diesel::joinable!(twofactor -> users (user_uuid));
|
||||||
|
diesel::joinable!(twofactor_incomplete -> users (user_uuid));
|
||||||
|
diesel::joinable!(users_collections -> collections (collection_uuid));
|
||||||
|
diesel::joinable!(users_collections -> users (user_uuid));
|
||||||
|
diesel::joinable!(users_organizations -> organizations (org_uuid));
|
||||||
|
diesel::joinable!(users_organizations -> users (user_uuid));
|
||||||
|
diesel::joinable!(web_authn_credentials -> users (user_uuid));
|
||||||
|
|
||||||
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
|
attachments,
|
||||||
|
auth_requests,
|
||||||
|
ciphers,
|
||||||
|
ciphers_collections,
|
||||||
|
collections,
|
||||||
|
collections_groups,
|
||||||
|
devices,
|
||||||
|
emergency_access,
|
||||||
|
event,
|
||||||
|
favorites,
|
||||||
|
folders,
|
||||||
|
folders_ciphers,
|
||||||
|
groups,
|
||||||
|
groups_users,
|
||||||
|
invitations,
|
||||||
|
org_policies,
|
||||||
|
organization_api_key,
|
||||||
|
organizations,
|
||||||
|
sends,
|
||||||
|
twofactor,
|
||||||
|
twofactor_duo_ctx,
|
||||||
|
twofactor_incomplete,
|
||||||
|
users,
|
||||||
|
users_collections,
|
||||||
|
users_organizations,
|
||||||
|
web_authn_credentials,
|
||||||
|
);
|
|
@ -320,6 +320,19 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
web_authn_credentials (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Text,
|
||||||
|
name -> Text,
|
||||||
|
credential -> Text,
|
||||||
|
supports_prf -> Bool,
|
||||||
|
encrypted_user_key -> Text,
|
||||||
|
encrypted_public_key -> Text,
|
||||||
|
encrypted_private_key -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
joinable!(attachments -> ciphers (cipher_uuid));
|
joinable!(attachments -> ciphers (cipher_uuid));
|
||||||
joinable!(ciphers -> organizations (organization_uuid));
|
joinable!(ciphers -> organizations (organization_uuid));
|
||||||
joinable!(ciphers -> users (user_uuid));
|
joinable!(ciphers -> users (user_uuid));
|
||||||
|
@ -348,6 +361,7 @@ joinable!(collections_groups -> collections (collections_uuid));
|
||||||
joinable!(collections_groups -> groups (groups_uuid));
|
joinable!(collections_groups -> groups (groups_uuid));
|
||||||
joinable!(event -> users_organizations (uuid));
|
joinable!(event -> users_organizations (uuid));
|
||||||
joinable!(auth_requests -> users (user_uuid));
|
joinable!(auth_requests -> users (user_uuid));
|
||||||
|
joinable!(web_authn_credentials -> users (user_uuid));
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
allow_tables_to_appear_in_same_query!(
|
||||||
attachments,
|
attachments,
|
||||||
|
@ -372,4 +386,5 @@ allow_tables_to_appear_in_same_query!(
|
||||||
collections_groups,
|
collections_groups,
|
||||||
event,
|
event,
|
||||||
auth_requests,
|
auth_requests,
|
||||||
|
web_authn_credentials,
|
||||||
);
|
);
|
||||||
|
|
|
@ -25,9 +25,9 @@ app-root form.ng-untouched button.\!tw-text-primary-600:nth-child(4) {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
/* Hide Log in with passkey on the login page */
|
/* Hide Log in with passkey on the login page */
|
||||||
app-root form.ng-untouched > div > div > button.\!tw-text-primary-600:nth-child(3) {
|
/* app-root form.ng-untouched > div > div > button.\!tw-text-primary-600:nth-child(3) { */
|
||||||
@extend %vw-hide;
|
/* @extend %vw-hide; */
|
||||||
}
|
/* } */
|
||||||
/* Hide the or text followed by the two buttons hidden above */
|
/* Hide the or text followed by the two buttons hidden above */
|
||||||
app-root form.ng-untouched > div:nth-child(1) > div:nth-child(3) > div:nth-child(2) {
|
app-root form.ng-untouched > div:nth-child(1) > div:nth-child(3) > div:nth-child(2) {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue