@ -1,9 +1,3 @@
use rocket ::serde ::json ::Json ;
use rocket ::Route ;
use serde_json ::Value ;
use url ::Url ;
use webauthn_rs ::{ base64_data ::Base64UrlSafeData , proto ::* , AuthenticationState , RegistrationState , Webauthn } ;
use crate ::{
use crate ::{
api ::{
api ::{
core ::{ log_user_event , two_factor ::_generate_recover_code } ,
core ::{ log_user_event , two_factor ::_generate_recover_code } ,
@ -18,6 +12,38 @@ use crate::{
util ::NumberOrString ,
util ::NumberOrString ,
CONFIG ,
CONFIG ,
} ;
} ;
use rocket ::serde ::json ::Json ;
use rocket ::Route ;
use serde_json ::Value ;
use std ::str ::FromStr ;
use std ::sync ::{ Arc , LazyLock } ;
use std ::time ::Duration ;
use url ::Url ;
use uuid ::Uuid ;
use webauthn_rs ::prelude ::{ Base64UrlSafeData , SecurityKey , SecurityKeyAuthentication , SecurityKeyRegistration } ;
use webauthn_rs ::{ Webauthn , WebauthnBuilder } ;
use webauthn_rs_proto ::{
AuthenticationExtensionsClientOutputs , AuthenticatorAssertionResponseRaw , AuthenticatorAttestationResponseRaw ,
PublicKeyCredential , RegisterPublicKeyCredential , RegistrationExtensionsClientOutputs ,
RequestAuthenticationExtensions ,
} ;
pub static WEBAUTHN_2FA_CONFIG : LazyLock < Arc < Webauthn > > = LazyLock ::new ( | | {
let domain = CONFIG . domain ( ) ;
let domain_origin = CONFIG . domain_origin ( ) ;
let rp_id = Url ::parse ( & domain ) . map ( | u | u . domain ( ) . map ( str ::to_owned ) ) . ok ( ) . flatten ( ) . unwrap_or_default ( ) ;
let rp_origin = Url ::parse ( & domain_origin ) . unwrap ( ) ;
let webauthn = WebauthnBuilder ::new ( & rp_id , & rp_origin )
. expect ( "Creating WebauthnBuilder failed" )
. rp_name ( & domain )
. timeout ( Duration ::from_millis ( 60000 ) )
. danger_set_user_presence_only_security_keys ( true ) ;
Arc ::new ( webauthn . build ( ) . expect ( "Building Webauthn failed" ) )
} ) ;
pub type Webauthn2FaConfig < 'a > = & 'a rocket ::State < Arc < Webauthn > > ;
pub fn routes ( ) -> Vec < Route > {
pub fn routes ( ) -> Vec < Route > {
routes ! [ get_webauthn , generate_webauthn_challenge , activate_webauthn , activate_webauthn_put , delete_webauthn , ]
routes ! [ get_webauthn , generate_webauthn_challenge , activate_webauthn , activate_webauthn_put , delete_webauthn , ]
@ -45,52 +71,13 @@ pub struct U2FRegistration {
pub migrated : Option < bool > ,
pub migrated : Option < bool > ,
}
}
struct WebauthnConfig {
url : String ,
origin : Url ,
rpid : String ,
}
impl WebauthnConfig {
fn load ( ) -> Webauthn < Self > {
let domain = CONFIG . domain ( ) ;
let domain_origin = CONFIG . domain_origin ( ) ;
Webauthn ::new ( Self {
rpid : Url ::parse ( & domain ) . map ( | u | u . domain ( ) . map ( str ::to_owned ) ) . ok ( ) . flatten ( ) . unwrap_or_default ( ) ,
url : domain ,
origin : Url ::parse ( & domain_origin ) . unwrap ( ) ,
} )
}
}
impl webauthn_rs ::WebauthnConfig for WebauthnConfig {
fn get_relying_party_name ( & self ) -> & str {
& self . url
}
fn get_origin ( & self ) -> & Url {
& self . origin
}
fn get_relying_party_id ( & self ) -> & str {
& self . rpid
}
/// 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
}
}
#[ derive(Debug, Serialize, Deserialize) ]
#[ derive(Debug, Serialize, Deserialize) ]
pub struct WebauthnRegistration {
pub struct WebauthnRegistration {
pub id : i32 ,
pub id : i32 ,
pub name : String ,
pub name : String ,
pub migrated : bool ,
pub migrated : bool ,
pub credential : Credential ,
pub credential : SecurityKey ,
}
}
impl WebauthnRegistration {
impl WebauthnRegistration {
@ -125,7 +112,12 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn:
}
}
#[ 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 ,
webauthn : Webauthn2FaConfig < '_ > ,
mut conn : DbConn ,
) -> JsonResult {
let data : PasswordOrOtpData = data . into_inner ( ) ;
let data : PasswordOrOtpData = data . into_inner ( ) ;
let user = headers . user ;
let user = headers . user ;
@ -135,13 +127,13 @@ async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Hea
. await ?
. await ?
. 1
. 1
. into_iter ( )
. into_iter ( )
. map ( | r | r . credential . cred_id ) // We return the credentialIds to the clients to avoid double registering
. map ( | r | r . credential . cred_id ( ) . to_owned ( ) ) // 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 ) = webauthn . start_securitykey_registration (
user . uuid . as_bytes ( ) . to_vec ( ) ,
Uuid ::from_str ( & user . uuid ) . expect ( "Failed to parse UUID" ) , // Should never fail
user . email ,
& user . email ,
user . name ,
& user . name ,
Some ( registrations ) ,
Some ( registrations ) ,
None ,
None ,
None ,
None ,
@ -193,8 +185,10 @@ impl From<RegisterPublicKeyCredentialCopy> for RegisterPublicKeyCredential {
response : AuthenticatorAttestationResponseRaw {
response : AuthenticatorAttestationResponseRaw {
attestation_object : r . response . attestation_object ,
attestation_object : r . response . attestation_object ,
client_data_json : r . response . client_data_json ,
client_data_json : r . response . client_data_json ,
transports : None ,
} ,
} ,
type_ : r . r#type ,
type_ : r . r#type ,
extensions : RegistrationExtensionsClientOutputs ::default ( ) ,
}
}
}
}
}
}
@ -205,7 +199,7 @@ pub struct PublicKeyCredentialCopy {
pub id : String ,
pub id : String ,
pub raw_id : Base64UrlSafeData ,
pub raw_id : Base64UrlSafeData ,
pub response : AuthenticatorAssertionResponseRawCopy ,
pub response : AuthenticatorAssertionResponseRawCopy ,
pub extensions : Option < AuthenticationExtensionsClientOutputs > ,
pub extensions : AuthenticationExtensionsClientOutputs ,
pub r#type : String ,
pub r#type : String ,
}
}
@ -238,7 +232,12 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
}
}
#[ 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 ,
webauthn : Webauthn2FaConfig < '_ > ,
mut conn : DbConn ,
) -> JsonResult {
let data : EnableWebauthnData = data . into_inner ( ) ;
let data : EnableWebauthnData = data . into_inner ( ) ;
let mut user = headers . user ;
let mut user = headers . user ;
@ -253,7 +252,7 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut
let type_ = TwoFactorType ::WebauthnRegisterChallenge as i32 ;
let type_ = TwoFactorType ::WebauthnRegisterChallenge as i32 ;
let state = match TwoFactor ::find_by_user_and_type ( & user . uuid , type_ , & mut conn ) . await {
let state = match TwoFactor ::find_by_user_and_type ( & user . uuid , type_ , & mut conn ) . await {
Some ( tf ) = > {
Some ( tf ) = > {
let state : RegistrationState = serde_json ::from_str ( & tf . data ) ? ;
let state : SecurityKey Registration = serde_json ::from_str ( & tf . data ) ? ;
tf . delete ( & mut conn ) . await ? ;
tf . delete ( & mut conn ) . await ? ;
state
state
}
}
@ -261,8 +260,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 = webauthn . finish_securitykey_registration ( & data . device_response . into ( ) , & state ) ? ;
WebauthnConfig ::load ( ) . 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
@ -291,8 +289,13 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut
}
}
#[ put( " /two-factor/webauthn " , data = " <data> " ) ]
#[ put( " /two-factor/webauthn " , data = " <data> " ) ]
async fn activate_webauthn_put ( data : Json < EnableWebauthnData > , headers : Headers , conn : DbConn ) -> JsonResult {
async fn activate_webauthn_put (
activate_webauthn ( data , headers , conn ) . await
data : Json < EnableWebauthnData > ,
headers : Headers ,
webauthn : Webauthn2FaConfig < '_ > ,
conn : DbConn ,
) -> JsonResult {
activate_webauthn ( data , headers , webauthn , conn ) . await
}
}
#[ derive(Debug, Deserialize) ]
#[ derive(Debug, Deserialize) ]
@ -335,7 +338,7 @@ async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, mut conn:
Err ( _ ) = > err ! ( "Error parsing U2F data" ) ,
Err ( _ ) = > err ! ( "Error parsing U2F data" ) ,
} ;
} ;
data . retain ( | r | r . reg . key_handle ! = removed_item . credential . cred_id ) ;
data . retain ( | r | r . reg . key_handle ! = removed_item . credential . cred_id ( ) . as_slice ( ) ) ;
let new_data_str = serde_json ::to_string ( & data ) ? ;
let new_data_str = serde_json ::to_string ( & data ) ? ;
u2f . data = new_data_str ;
u2f . data = new_data_str ;
@ -362,18 +365,36 @@ pub async fn get_webauthn_registrations(
}
}
}
}
pub async fn generate_webauthn_login ( user_id : & UserId , conn : & mut DbConn ) -> JsonResult {
pub async fn generate_webauthn_login (
user_id : & UserId ,
webauthn : Webauthn2FaConfig < '_ > ,
conn : & mut DbConn ,
) -> JsonResult {
// Load saved credentials
// Load saved credentials
let creds : Vec < Credential > =
let creds : Vec < _ > = get_webauthn_registrations ( user_id , conn ) . await ? . 1. into_iter ( ) . map ( | r | r . credential ) . collect ( ) ;
get_webauthn_registrations ( user_id , conn ) . await ? . 1. into_iter ( ) . map ( | r | r . credential ) . collect ( ) ;
if creds . is_empty ( ) {
if creds . is_empty ( ) {
err ! ( "No Webauthn devices registered" )
err ! ( "No Webauthn devices registered" )
}
}
// 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 ( mut response , state ) = webauthn . start_securitykey_authentication ( & creds ) ? ;
let ( response , state ) = WebauthnConfig ::load ( ) . generate_challenge_authenticate_options ( creds , Some ( ext ) ) ? ;
// Modify to discourage user verification
let mut state = serde_json ::to_value ( & state ) ? ;
// Add appid, this is only needed for U2F compatibility, so maybe it can be removed as well
let app_id = format ! ( "{}/app-id.json" , & CONFIG . domain ( ) ) ;
state [ "ast" ] [ "appid" ] = Value ::String ( app_id . clone ( ) ) ;
response
. public_key
. extensions
. get_or_insert ( RequestAuthenticationExtensions {
appid : None ,
uvm : None ,
hmac_get_secret : None ,
} )
. appid = Some ( app_id ) ;
// 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 ) ? )
@ -384,11 +405,16 @@ pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> Jso
Ok ( Json ( serde_json ::to_value ( response . public_key ) ? ) )
Ok ( Json ( serde_json ::to_value ( response . public_key ) ? ) )
}
}
pub async fn validate_webauthn_login ( user_id : & UserId , response : & str , conn : & mut DbConn ) -> EmptyResult {
pub async fn validate_webauthn_login (
user_id : & UserId ,
response : & str ,
webauthn : Webauthn2FaConfig < '_ > ,
conn : & mut DbConn ,
) -> EmptyResult {
let type_ = TwoFactorType ::WebauthnLoginChallenge as i32 ;
let type_ = TwoFactorType ::WebauthnLoginChallenge as i32 ;
let state = match TwoFactor ::find_by_user_and_type ( user_id , type_ , conn ) . await {
let state = match TwoFactor ::find_by_user_and_type ( user_id , type_ , conn ) . await {
Some ( tf ) = > {
Some ( tf ) = > {
let state : AuthenticationState = serde_json ::from_str ( & tf . data ) ? ;
let state : SecurityKey Authentication = serde_json ::from_str ( & tf . data ) ? ;
tf . delete ( conn ) . await ? ;
tf . delete ( conn ) . await ? ;
state
state
}
}
@ -405,13 +431,11 @@ pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mu
let mut registrations = get_webauthn_registrations ( user_id , conn ) . await ? . 1 ;
let mut registrations = get_webauthn_registrations ( user_id , conn ) . await ? . 1 ;
// If the credential we received is migrated from U2F, enable the U2F compatibility
let authentication_result = webauthn . finish_securitykey_authentication ( & rsp , & state ) ? ;
//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 {
for reg in & mut registrations {
if & reg . credential . cred_id = = cred_id {
if reg . credential . cred_id ( ) = = auth entic ation_resu lt . cred_id ( ) & & authentication_result . needs_update ( ) {
reg . credential . counter = auth_data . counter ;
reg . credential . update_credential ( & authentication_result ) ;
TwoFactor ::new ( user_id . clone ( ) , TwoFactorType ::Webauthn , serde_json ::to_string ( & registrations ) ? )
TwoFactor ::new ( user_id . clone ( ) , TwoFactorType ::Webauthn , serde_json ::to_string ( & registrations ) ? )
. save ( conn )
. save ( conn )