@ -1,4 +1,4 @@
use chrono ::{ NaiveDateTime , Utc } ;
use chrono ::Utc ;
use num_traits ::FromPrimitive ;
use rocket ::{
form ::{ Form , FromForm } ,
@ -24,14 +24,14 @@ use crate::{
auth ::{ generate_organization_api_key_login_claims , AuthMethod , ClientHeaders , ClientIp , ClientVersion } ,
db ::{
models ::{
AuthRequest , AuthRequestId , Device , DeviceId , EventType , Invitation , OrganizationApiKey , OrganizationId ,
SsoNonce , SsoUser , TwoFactor , TwoFactorIncomplete , TwoFactorType , User , UserId ,
AuthRequest , AuthRequestId , Device , DeviceId , EventType , Invitation , OIDCCodeWrapper , OrganizationApiKey ,
OrganizationId , SsoAuth , SsoUser , TwoFactor , TwoFactorIncomplete , TwoFactorType , User , UserId ,
} ,
DbConn ,
} ,
error ::MapResult ,
mail , sso ,
sso ::{ OIDCCode , OIDCState } ,
sso ::{ OIDCCode , OIDCCodeChallenge , OIDCCodeVerifier , OIDC State } ,
util , CONFIG ,
} ;
@ -92,6 +92,7 @@ async fn login(
"authorization_code" if CONFIG . sso_enabled ( ) = > {
_check_is_some ( & data . client_id , "client_id cannot be blank" ) ? ;
_check_is_some ( & data . code , "code cannot be blank" ) ? ;
_check_is_some ( & data . code_verifier , "code verifier 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" ) ? ;
@ -147,7 +148,7 @@ async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> Json
}
Ok ( ( mut device , auth_tokens ) ) = > {
// Save to update `device.updated_at` to track usage and toggle new status
device . save ( conn ) . await ? ;
device . save ( true , conn ) . await ? ;
let result = json ! ( {
"refresh_token" : auth_tokens . refresh_token ( ) ,
@ -175,17 +176,23 @@ async fn _sso_login(
// Ratelimit the login
crate ::ratelimit ::check_limit_login ( & ip . ip ) ? ;
let code = match data . code . as_ref ( ) {
None = > err ! (
let ( state , code_verifier ) = match ( data . code . as_ref ( ) , data . code_verifier . as_ref ( ) ) {
( None , _ ) = > err ! (
"Got no code in OIDC data" ,
ErrorEvent {
event : EventType ::UserFailedLogIn
}
) ,
Some ( code ) = > code ,
( _ , None ) = > err ! (
"Got no code verifier in OIDC data" ,
ErrorEvent {
event : EventType ::UserFailedLogIn
}
) ,
( Some ( code ) , Some ( code_verifier ) ) = > ( code , code_verifier . clone ( ) ) ,
} ;
let user_infos = sso ::exchange_code ( code , conn ) . await ? ;
let ( sso_auth , user_infos ) = sso ::exchange_code ( state , code_verifier , conn ) . await ? ;
let user_with_sso = match SsoUser ::find_by_identifier ( & user_infos . identifier , conn ) . await {
None = > match SsoUser ::find_by_mail ( & user_infos . email , conn ) . await {
None = > None ,
@ -248,7 +255,7 @@ async fn _sso_login(
_ = > ( ) ,
}
let mut user = User ::new ( & user_infos . email , user_infos . user_name ) ;
let mut user = User ::new ( & user_infos . email , user_infos . user_name . clone ( ) ) ;
user . verified_at = Some ( now ) ;
user . save ( conn ) . await ? ;
@ -267,13 +274,14 @@ async fn _sso_login(
}
Some ( ( mut user , sso_user ) ) = > {
let mut device = get_device ( & data , conn , & user ) . await ? ;
let twofactor_token = twofactor_auth ( & mut user , & data , & mut device , ip , client_version , conn ) . await ? ;
if user . private_key . is_none ( ) {
// User was invited a stub was created
user . verified_at = Some ( now ) ;
if let Some ( user_name ) = user_infos . user_name {
user . name = user_name ;
if let Some ( ref user_name ) = user_infos . user_name {
user . name = user_name . clone ( ) ;
}
user . save ( conn ) . await ? ;
@ -290,30 +298,13 @@ async fn _sso_login(
}
} ;
// We passed 2FA get full user information
let auth_user = sso ::redeem ( & user_infos . state , conn ) . await ? ;
if sso_user . is_none ( ) {
let user_sso = SsoUser {
user_uuid : user . uuid . clone ( ) ,
identifier : user_infos . identifier ,
} ;
user_sso . save ( conn ) . await ? ;
}
// Set the user_uuid here to be passed back used for event logging.
* user_id = Some ( user . uuid . clone ( ) ) ;
let auth_tokens = sso ::create_auth_tokens (
& device ,
& user ,
data . client_id ,
auth_user . refresh_token ,
auth_user . access_token ,
auth_user . expires_in ,
) ? ;
// We passed 2FA get auth tokens
let auth_tokens = sso ::redeem ( & device , & user , data . client_id , sso_user , sso_auth , user_infos , conn ) . await ? ;
authenticated_response ( & user , & mut device , auth_tokens , twofactor_token , & now , conn , ip ) . await
authenticated_response ( & user , & mut device , auth_tokens , twofactor_token , conn , ip ) . await
}
async fn _password_login (
@ -435,7 +426,7 @@ async fn _password_login(
let auth_tokens = auth ::AuthTokens ::new ( & device , & user , AuthMethod ::Password , data . client_id ) ;
authenticated_response ( & user , & mut device , auth_tokens , twofactor_token , & now , conn , ip ) . await
authenticated_response ( & user , & mut device , auth_tokens , twofactor_token , conn , ip ) . await
}
async fn authenticated_response (
@ -443,12 +434,12 @@ async fn authenticated_response(
device : & mut Device ,
auth_tokens : auth ::AuthTokens ,
twofactor_token : Option < String > ,
now : & NaiveDateTime ,
conn : & DbConn ,
ip : & ClientIp ,
) -> JsonResult {
if CONFIG . mail_enabled ( ) & & device . is_new ( ) {
if let Err ( e ) = mail ::send_new_device_logged_in ( & user . email , & ip . ip . to_string ( ) , now , device ) . await {
let now = Utc ::now ( ) . naive_utc ( ) ;
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 ( ) {
@ -468,7 +459,7 @@ async fn authenticated_response(
}
// Save to update `device.updated_at` to track usage and toggle new status
device . save ( conn ) . await ? ;
device . save ( true , conn ) . await ? ;
let master_password_policy = master_password_policy ( user , conn ) . await ;
@ -585,7 +576,7 @@ async fn _user_api_key_login(
let access_claims = auth ::LoginJwtClaims ::default ( & device , & user , & AuthMethod ::UserApiKey , data . client_id ) ;
// Save to update `device.updated_at` to track usage and toggle new status
device . save ( conn ) . await ? ;
device . save ( true , conn ) . await ? ;
info ! ( "User {} logged in successfully via API key. IP: {}" , user . email , ip . ip ) ;
@ -648,7 +639,12 @@ async fn get_device(data: &ConnectData, conn: &DbConn, user: &User) -> ApiResult
// Find device or create new
match Device ::find_by_uuid_and_user ( & device_id , & user . uuid , conn ) . await {
Some ( device ) = > Ok ( device ) ,
None = > Device ::new ( device_id , user . uuid . clone ( ) , device_name , device_type , conn ) . await ,
None = > {
let mut device = Device ::new ( device_id , user . uuid . clone ( ) , device_name , device_type ) ;
// save device without updating `device.updated_at`
device . save ( false , conn ) . await ? ;
Ok ( device )
}
}
}
@ -997,9 +993,12 @@ struct ConnectData {
two_factor_remember : Option < i32 > ,
#[ field(name = uncased( " authrequest " )) ]
auth_request : Option < AuthRequestId > ,
// Needed for authorization code
#[ field(name = uncased( " code " )) ]
code : Option < String > ,
code : Option < OIDCState > ,
#[ field(name = uncased( " code_verifier " )) ]
code_verifier : Option < OIDCCodeVerifier > ,
}
fn _check_is_some < T > ( value : & Option < T > , msg : & str ) -> EmptyResult {
if value . is_none ( ) {
@ -1021,14 +1020,13 @@ fn prevalidate() -> JsonResult {
}
#[ get( " /connect/oidc-signin?<code>&<state> " , rank = 1) ]
async fn oidcsignin ( code : OIDCCode , state : String , conn : DbConn ) -> ApiResult < Redirect > {
oidcsignin_redirect (
async fn oidcsignin ( code : OIDCCode , state : String , mut conn : DbConn ) -> ApiResult < Redirect > {
_ oidcsignin_redirect(
state ,
| decoded_state | sso ::OIDCCodeWrapper ::Ok {
state : decoded_state ,
OIDCCodeWrapper ::Ok {
code ,
} ,
& conn ,
& mut conn ,
)
. await
}
@ -1040,42 +1038,44 @@ async fn oidcsignin_error(
state : String ,
error : String ,
error_description : Option < String > ,
conn : DbConn ,
mut conn : DbConn ,
) -> ApiResult < Redirect > {
oidcsignin_redirect (
_ oidcsignin_redirect(
state ,
| decoded_state | sso ::OIDCCodeWrapper ::Error {
state : decoded_state ,
OIDCCodeWrapper ::Error {
error ,
error_description ,
} ,
& conn ,
& mut conn ,
)
. await
}
// The state was encoded using Base64 to ensure no issue with providers.
// iss and scope parameters are needed for redirection to work on IOS.
async fn oidcsignin_redirect (
// We pass the state as the code to get it back later on.
async fn _oidcsignin_redirect (
base64_state : String ,
wrapper : impl FnOnce ( OIDCState ) -> sso ::OIDCCodeWrapper ,
conn : & DbConn ,
code_response : OIDCCodeWrapper ,
conn : & mut DbConn ,
) -> ApiResult < Redirect > {
let state = sso ::decode_state ( & base64_state ) ? ;
let code = sso ::encode_code_claims ( wrapper ( state . clone ( ) ) ) ;
let nonce = match SsoNonce ::find ( & state , conn ) . await {
Some ( n ) = > n ,
None = > err ! ( format ! ( "Failed to retrieve redirect_uri with {state}" ) ) ,
let mut sso_auth = match SsoAuth ::find ( & state , conn ) . await {
None = > err ! ( format ! ( "Cannot retrieve sso_auth for {state}" ) ) ,
Some ( sso_auth ) = > sso_auth ,
} ;
sso_auth . code_response = Some ( code_response ) ;
sso_auth . updated_at = Utc ::now ( ) . naive_utc ( ) ;
sso_auth . save ( conn ) . await ? ;
let mut url = match url ::Url ::parse ( & nonce . redirect_uri ) {
let mut url = match url ::Url ::parse ( & sso_auth . redirect_uri ) {
Ok ( url ) = > url ,
Err ( err ) = > err ! ( format ! ( "Failed to parse redirect uri ({}): {err}" , nonce . redirect_uri ) ) ,
Err ( err ) = > err ! ( format ! ( "Failed to parse redirect uri ({}): {err}" , sso_auth . redirect_uri ) ) ,
} ;
url . query_pairs_mut ( )
. append_pair ( "code" , & cod e)
. append_pair ( "code" , & stat e)
. append_pair ( "state" , & state )
. append_pair ( "scope" , & AuthMethod ::Sso . scope ( ) )
. append_pair ( "iss" , & CONFIG . domain ( ) ) ;
@ -1098,10 +1098,8 @@ struct AuthorizeData {
#[ allow(unused) ]
scope : Option < String > ,
state : OIDCState ,
#[ allow(unused) ]
code_challenge : Option < String > ,
#[ allow(unused) ]
code_challenge_method : Option < String > ,
code_challenge : OIDCCodeChallenge ,
code_challenge_method : String ,
#[ allow(unused) ]
response_mode : Option < String > ,
#[ allow(unused) ]
@ -1118,10 +1116,16 @@ async fn authorize(data: AuthorizeData, conn: DbConn) -> ApiResult<Redirect> {
client_id ,
redirect_uri ,
state ,
code_challenge ,
code_challenge_method ,
. .
} = data ;
let auth_url = sso ::authorize_url ( state , & client_id , & redirect_uri , conn ) . await ? ;
if code_challenge_method ! = "S256" {
err ! ( "Unsupported code challenge method" ) ;
}
let auth_url = sso ::authorize_url ( state , code_challenge , & client_id , & redirect_uri , conn ) . await ? ;
Ok ( Redirect ::temporary ( String ::from ( auth_url ) ) )
}