@ -1,8 +1,10 @@
use chrono ::Utc ;
use chrono ::Utc ;
use jsonwebtoken ::DecodingKey ;
use num_traits ::FromPrimitive ;
use num_traits ::FromPrimitive ;
use rocket ::serde ::json ::Json ;
use rocket ::serde ::json ::Json ;
use rocket ::{
use rocket ::{
form ::{ Form , FromForm } ,
form ::{ Form , FromForm } ,
http ::CookieJar ,
Route ,
Route ,
} ;
} ;
use serde_json ::Value ;
use serde_json ::Value ;
@ -17,14 +19,16 @@ use crate::{
push ::register_push_device ,
push ::register_push_device ,
ApiResult , EmptyResult , JsonResult ,
ApiResult , EmptyResult , JsonResult ,
} ,
} ,
auth ::{ generate_organization_api_key_login_claims, ClientHeaders , ClientIp } ,
auth ::{ encode_jwt, generate_organization_api_key_login_claims, generate_ssotoke n_claims, ClientHeaders , ClientIp } ,
db ::{ models ::* , DbConn } ,
db ::{ models ::* , DbConn } ,
error ::MapResult ,
error ::MapResult ,
mail , util , CONFIG ,
mail , util ,
util ::{ CookieManager , CustomRedirect } ,
CONFIG ,
} ;
} ;
pub fn routes ( ) -> Vec < Route > {
pub fn routes ( ) -> Vec < Route > {
routes ! [ login , prelogin , identity_register ]
routes ! [ login , prelogin , identity_register , prevalidate , authorize , oidcsignin ]
}
}
#[ post( " /connect/token " , data = " <data> " ) ]
#[ post( " /connect/token " , data = " <data> " ) ]
@ -61,6 +65,15 @@ async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn:
_api_key_login ( data , & mut user_uuid , & mut conn , & client_header . ip ) . await
_api_key_login ( data , & mut user_uuid , & mut conn , & client_header . ip ) . await
}
}
"authorization_code" = > {
_check_is_some ( & data . client_id , "client_id cannot be blank" ) ? ;
_check_is_some ( & data . code , "code 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" ) ? ;
_authorization_login ( data , & mut user_uuid , & mut conn , & client_header . ip ) . await
}
t = > err ! ( "Invalid type" , t ) ,
t = > err ! ( "Invalid type" , t ) ,
} ;
} ;
@ -127,6 +140,141 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
Ok ( Json ( result ) )
Ok ( Json ( result ) )
}
}
#[ derive(Debug, Serialize, Deserialize) ]
struct TokenPayload {
exp : i64 ,
email : Option < String > ,
nonce : String ,
}
async fn _authorization_login (
data : ConnectData ,
user_uuid : & mut Option < String > ,
conn : & mut DbConn ,
ip : & ClientIp ,
) -> JsonResult {
let scope = match data . scope . as_ref ( ) {
None = > err ! ( "Got no scope in OIDC data" ) ,
Some ( scope ) = > scope ,
} ;
if scope ! = "api offline_access" {
err ! ( "Scope not supported" )
}
let scope_vec = vec! [ "api" . into ( ) , "offline_access" . into ( ) ] ;
let code = match data . code . as_ref ( ) {
None = > err ! ( "Got no code in OIDC data" ) ,
Some ( code ) = > code ,
} ;
let ( refresh_token , id_token , user_info ) = match get_auth_code_access_token ( code ) . await {
Ok ( ( refresh_token , id_token , user_info ) ) = > ( refresh_token , id_token , user_info ) ,
Err ( _err ) = > err ! ( "Could not retrieve access token" ) ,
} ;
let mut validation = jsonwebtoken ::Validation ::default ( ) ;
validation . insecure_disable_signature_validation ( ) ;
let token =
match jsonwebtoken ::decode ::< TokenPayload > ( id_token . as_str ( ) , & DecodingKey ::from_secret ( & [ ] ) , & validation ) {
Err ( _err ) = > err ! ( "Could not decode id token" ) ,
Ok ( payload ) = > payload . claims ,
} ;
// let expiry = token.exp;
let nonce = token . nonce ;
let mut new_user = false ;
match SsoNonce ::find ( & nonce , conn ) . await {
Some ( sso_nonce ) = > {
match sso_nonce . delete ( conn ) . await {
Ok ( _ ) = > {
let user_email = match token . email {
Some ( email ) = > email ,
None = > match user_info . email ( ) {
None = > err ! ( "Neither id token nor userinfo contained an email" ) ,
Some ( email ) = > email . to_owned ( ) . to_string ( ) ,
} ,
} ;
let now = Utc ::now ( ) . naive_utc ( ) ;
let mut user = match User ::find_by_mail ( & user_email , conn ) . await {
Some ( user ) = > user ,
None = > {
new_user = true ;
User ::new ( user_email . clone ( ) )
}
} ;
if new_user {
user . verified_at = Some ( Utc ::now ( ) . naive_utc ( ) ) ;
user . save ( conn ) . await ? ;
}
// Set the user_uuid here to be passed back used for event logging.
* user_uuid = Some ( user . uuid . clone ( ) ) ;
let ( mut device , new_device ) = get_device ( & data , conn , & user ) . await ;
let twofactor_token = twofactor_auth ( & user , & data , & mut device , ip , true , conn ) . await ? ;
if CONFIG . mail_enabled ( ) & & new_device {
if let Err ( e ) =
mail ::send_new_device_logged_in ( & user . email , & ip . ip . to_string ( ) , & now , & device . name ) . await
{
error ! ( "Error sending new device email: {:#?}" , e ) ;
if CONFIG . require_device_email ( ) {
err ! ( "Could not send login notification email. Please contact your administrator." )
}
}
}
if CONFIG . sso_acceptall_invites ( ) {
for user_org in UserOrganization ::find_invited_by_user ( & user . uuid , conn ) . await . iter_mut ( ) {
user_org . status = UserOrgStatus ::Accepted as i32 ;
user_org . save ( conn ) . await ? ;
}
}
device . refresh_token = refresh_token . clone ( ) ;
device . save ( conn ) . await ? ;
let ( access_token , expires_in ) = device . refresh_tokens ( & user , scope_vec ) ;
device . save ( conn ) . await ? ;
let mut result = json ! ( {
"access_token" : access_token ,
"token_type" : "Bearer" ,
"refresh_token" : device . refresh_token ,
"expires_in" : expires_in ,
"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" : user . password_hash . is_empty ( ) ,
"scope" : scope ,
"unofficialServer" : true ,
} ) ;
if let Some ( token ) = twofactor_token {
result [ "TwoFactorToken" ] = Value ::String ( token ) ;
}
info ! ( "User {} logged in successfully. IP: {}" , user . email , ip . ip ) ;
Ok ( Json ( result ) )
}
Err ( _ ) = > err ! ( "Failed to delete nonce" ) ,
}
}
None = > {
err ! ( "Invalid nonce" )
}
}
}
#[ derive(Default, Deserialize, Serialize) ]
#[ derive(Default, Deserialize, Serialize) ]
#[ serde(rename_all = " camelCase " ) ]
#[ serde(rename_all = " camelCase " ) ]
struct MasterPasswordPolicy {
struct MasterPasswordPolicy {
@ -155,6 +303,10 @@ async fn _password_login(
// Ratelimit the login
// Ratelimit the login
crate ::ratelimit ::check_limit_login ( & ip . ip ) ? ;
crate ::ratelimit ::check_limit_login ( & ip . ip ) ? ;
if CONFIG . sso_enabled ( ) & & CONFIG . sso_only ( ) {
err ! ( "SSO sign-in is required" ) ;
}
// Get the user
// Get the user
let username = data . username . as_ref ( ) . unwrap ( ) . trim ( ) ;
let username = data . username . as_ref ( ) . unwrap ( ) . trim ( ) ;
let mut user = match User ::find_by_mail ( username , conn ) . await {
let mut user = match User ::find_by_mail ( username , conn ) . await {
@ -254,7 +406,7 @@ async fn _password_login(
let ( mut device , new_device ) = get_device ( & data , conn , & user ) . await ;
let ( mut device , new_device ) = get_device ( & data , conn , & user ) . await ;
let twofactor_token = twofactor_auth ( & user , & data , & mut device , ip , conn ) . await ? ;
let twofactor_token = twofactor_auth ( & user , & data , & mut device , ip , false , conn ) . await ? ;
if CONFIG . mail_enabled ( ) & & new_device {
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 {
if let Err ( e ) = mail ::send_new_device_logged_in ( & user . email , & ip . ip . to_string ( ) , & now , & device ) . await {
@ -514,6 +666,7 @@ async fn twofactor_auth(
data : & ConnectData ,
data : & ConnectData ,
device : & mut Device ,
device : & mut Device ,
ip : & ClientIp ,
ip : & ClientIp ,
is_sso : bool ,
conn : & mut DbConn ,
conn : & mut DbConn ,
) -> ApiResult < Option < String > > {
) -> ApiResult < Option < String > > {
let twofactors = TwoFactor ::find_by_user ( & user . uuid , conn ) . await ;
let twofactors = TwoFactor ::find_by_user ( & user . uuid , conn ) . await ;
@ -532,7 +685,15 @@ async fn twofactor_auth(
let twofactor_code = match data . two_factor_token {
let twofactor_code = match data . two_factor_token {
Some ( ref code ) = > code ,
Some ( ref code ) = > code ,
None = > {
None = > {
err_json ! ( _json_err_twofactor ( & twofactor_ids , & user . uuid , data , conn ) . await ? , "2FA token not provided" )
if is_sso {
if CONFIG . sso_only ( ) {
err ! ( "2FA not supported with SSO login, contact your administrator" ) ;
} else {
err ! ( "2FA not supported with SSO login, log in directly using email and master password" ) ;
}
} else {
err_json ! ( _json_err_twofactor ( & twofactor_ids , & user . uuid , data , conn ) . await ? , "2FA token not provided" ) ;
}
}
}
} ;
} ;
@ -766,11 +927,187 @@ struct ConnectData {
two_factor_remember : Option < i32 > ,
two_factor_remember : Option < i32 > ,
#[ field(name = uncased( " authrequest " )) ]
#[ field(name = uncased( " authrequest " )) ]
auth_request : Option < String > ,
auth_request : Option < String > ,
// Needed for authorization code
#[ form(field = uncased( " code " )) ]
code : Option < String > ,
}
}
fn _check_is_some < T > ( value : & Option < T > , msg : & str ) -> EmptyResult {
fn _check_is_some < T > ( value : & Option < T > , msg : & str ) -> EmptyResult {
if value . is_none ( ) {
if value . is_none ( ) {
err ! ( msg )
err ! ( msg )
}
}
Ok ( ( ) )
Ok ( ( ) )
}
}
#[ get( " /account/prevalidate " ) ]
#[ allow(non_snake_case) ]
fn prevalidate ( ) -> JsonResult {
let claims = generate_ssotoken_claims ( ) ;
let ssotoken = encode_jwt ( & claims ) ;
Ok ( Json ( json ! ( {
"token" : ssotoken ,
} ) ) )
}
use openidconnect ::core ::{ CoreClient , CoreProviderMetadata , CoreResponseType , CoreUserInfoClaims } ;
use openidconnect ::reqwest ::async_http_client ;
use openidconnect ::{
AuthenticationFlow , AuthorizationCode , ClientId , ClientSecret , CsrfToken , IssuerUrl , Nonce , OAuth2TokenResponse ,
RedirectUrl , Scope ,
} ;
async fn get_client_from_sso_config ( ) -> ApiResult < CoreClient > {
let redirect = CONFIG . sso_callback_path ( ) ;
let client_id = ClientId ::new ( CONFIG . sso_client_id ( ) ) ;
let client_secret = ClientSecret ::new ( CONFIG . sso_client_secret ( ) ) ;
let issuer_url = match IssuerUrl ::new ( CONFIG . sso_authority ( ) ) {
Ok ( issuer ) = > issuer ,
Err ( _err ) = > err ! ( "invalid issuer URL" ) ,
} ;
let provider_metadata = match CoreProviderMetadata ::discover_async ( issuer_url , async_http_client ) . await {
Ok ( metadata ) = > metadata ,
Err ( _err ) = > {
err ! ( "Failed to discover OpenID provider" )
}
} ;
let redirect_uri = match RedirectUrl ::new ( redirect ) {
Ok ( uri ) = > uri ,
Err ( err ) = > err ! ( "Invalid redirection url: {}" , err . to_string ( ) ) ,
} ;
let client = CoreClient ::from_provider_metadata ( provider_metadata , client_id , Some ( client_secret ) )
. set_redirect_uri ( redirect_uri ) ;
Ok ( client )
}
#[ get( " /connect/oidc-signin?<code> " ) ]
fn oidcsignin ( code : String , jar : & CookieJar < ' _ > , _conn : DbConn ) -> ApiResult < CustomRedirect > {
let cookiemanager = CookieManager ::new ( jar ) ;
let redirect_uri = match cookiemanager . get_cookie ( "redirect_uri" . to_string ( ) ) {
None = > err ! ( "No redirect_uri in cookie" ) ,
Some ( uri ) = > uri ,
} ;
let orig_state = match cookiemanager . get_cookie ( "state" . to_string ( ) ) {
None = > err ! ( "No state in cookie" ) ,
Some ( state ) = > state ,
} ;
cookiemanager . delete_cookie ( "redirect_uri" . to_string ( ) ) ;
cookiemanager . delete_cookie ( "state" . to_string ( ) ) ;
let redirect = CustomRedirect {
url : format ! ( "{redirect_uri}?code={code}&state={orig_state}" ) ,
headers : vec ! [ ] ,
} ;
Ok ( redirect )
}
#[ derive(FromForm) ]
#[ allow(non_snake_case) ]
struct AuthorizeData {
#[ allow(unused) ]
#[ field(name = uncased( " client_id " )) ]
#[ field(name = uncased( " clientid " )) ]
client_id : Option < String > ,
#[ field(name = uncased( " redirect_uri " )) ]
#[ field(name = uncased( " redirecturi " )) ]
redirect_uri : Option < String > ,
#[ allow(unused) ]
#[ field(name = uncased( " response_type " )) ]
#[ field(name = uncased( " responsetype " )) ]
response_type : Option < String > ,
#[ allow(unused) ]
#[ field(name = uncased( " scope " )) ]
scope : Option < String > ,
#[ field(name = uncased( " state " )) ]
state : Option < String > ,
#[ allow(unused) ]
#[ field(name = uncased( " code_challenge " )) ]
code_challenge : Option < String > ,
#[ allow(unused) ]
#[ field(name = uncased( " code_challenge_method " )) ]
code_challenge_method : Option < String > ,
#[ allow(unused) ]
#[ field(name = uncased( " response_mode " )) ]
response_mode : Option < String > ,
#[ allow(unused) ]
#[ field(name = uncased( " domain_hint " )) ]
domain_hint : Option < String > ,
#[ allow(unused) ]
#[ field(name = uncased( " ssoToken " )) ]
ssoToken : Option < String > ,
}
#[ get( " /connect/authorize?<data..> " ) ]
async fn authorize ( data : AuthorizeData , jar : & CookieJar < ' _ > , mut conn : DbConn ) -> ApiResult < CustomRedirect > {
let cookiemanager = CookieManager ::new ( jar ) ;
match get_client_from_sso_config ( ) . await {
Ok ( client ) = > {
let ( auth_url , _csrf_state , nonce ) = client
. authorize_url (
AuthenticationFlow ::< CoreResponseType > ::AuthorizationCode ,
CsrfToken ::new_random ,
Nonce ::new_random ,
)
. add_scope ( Scope ::new ( "email" . to_string ( ) ) )
. add_scope ( Scope ::new ( "profile" . to_string ( ) ) )
. url ( ) ;
let sso_nonce = SsoNonce ::new ( nonce . secret ( ) . to_string ( ) ) ;
sso_nonce . save ( & mut conn ) . await ? ;
let redirect_uri = match data . redirect_uri {
None = > err ! ( "No redirect_uri in data" ) ,
Some ( uri ) = > uri ,
} ;
cookiemanager . set_cookie ( "redirect_uri" . to_string ( ) , redirect_uri ) ;
let state = match data . state {
None = > err ! ( "No state in data" ) ,
Some ( state ) = > state ,
} ;
cookiemanager . set_cookie ( "state" . to_string ( ) , state ) ;
let redirect = CustomRedirect {
url : format ! ( "{}" , auth_url ) ,
headers : vec ! [ ] ,
} ;
Ok ( redirect )
}
Err ( _err ) = > err ! ( "Unable to find client from identifier" ) ,
}
}
async fn get_auth_code_access_token ( code : & str ) -> ApiResult < ( String , String , CoreUserInfoClaims ) > {
let oidc_code = AuthorizationCode ::new ( String ::from ( code ) ) ;
match get_client_from_sso_config ( ) . await {
Ok ( client ) = > match client . exchange_code ( oidc_code ) . request_async ( async_http_client ) . await {
Ok ( token_response ) = > {
let refresh_token = match token_response . refresh_token ( ) {
Some ( token ) = > token . secret ( ) . to_string ( ) ,
None = > String ::new ( ) ,
} ;
let id_token = match token_response . extra_fields ( ) . id_token ( ) {
None = > err ! ( "Token response did not contain an id_token" ) ,
Some ( token ) = > token . to_string ( ) ,
} ;
let user_info : CoreUserInfoClaims =
match client . user_info ( token_response . access_token ( ) . to_owned ( ) , None ) {
Err ( _err ) = > err ! ( "Token response did not contain user_info" ) ,
Ok ( info ) = > match info . request_async ( async_http_client ) . await {
Err ( _err ) = > err ! ( "Request to user_info endpoint failed" ) ,
Ok ( claim ) = > claim ,
} ,
} ;
Ok ( ( refresh_token , id_token , user_info ) )
}
Err ( err ) = > err ! ( "Failed to contact token endpoint: {}" , err . to_string ( ) ) ,
} ,
Err ( _err ) = > err ! ( "Unable to find client" ) ,
}
}