@@ -170,9 +170,6 @@ pub enum RouteError {
170
170
#[ error( "user not found" ) ]
171
171
UserNotFound ,
172
172
173
- #[ error( "session not found" ) ]
174
- SessionNotFound ,
175
-
176
173
#[ error( "user has no password" ) ]
177
174
NoPassword ,
178
175
@@ -201,13 +198,11 @@ impl IntoResponse for RouteError {
201
198
fn into_response ( self ) -> axum:: response:: Response {
202
199
let event_id = sentry:: capture_error ( & self ) ;
203
200
let response = match self {
204
- Self :: Internal ( _) | Self :: SessionNotFound | Self :: ProvisionDeviceFailed ( _) => {
205
- MatrixError {
206
- errcode : "M_UNKNOWN" ,
207
- error : "Internal server error" ,
208
- status : StatusCode :: INTERNAL_SERVER_ERROR ,
209
- }
210
- }
201
+ Self :: Internal ( _) | Self :: ProvisionDeviceFailed ( _) => MatrixError {
202
+ errcode : "M_UNKNOWN" ,
203
+ error : "Internal server error" ,
204
+ status : StatusCode :: INTERNAL_SERVER_ERROR ,
205
+ } ,
211
206
Self :: RateLimited ( _) => MatrixError {
212
207
errcode : "M_LIMIT_EXCEEDED" ,
213
208
error : "Too many login attempts" ,
@@ -323,7 +318,17 @@ pub(crate) async fn post(
323
318
. await ?
324
319
}
325
320
326
- ( _, Credentials :: Token { token } ) => token_login ( & mut repo, & clock, & token) . await ?,
321
+ ( _, Credentials :: Token { token } ) => {
322
+ token_login (
323
+ & mut repo,
324
+ & clock,
325
+ & token,
326
+ input. device_id ,
327
+ & homeserver,
328
+ & mut rng,
329
+ )
330
+ . await ?
331
+ }
327
332
328
333
_ => {
329
334
return Err ( RouteError :: Unsupported ) ;
@@ -382,6 +387,9 @@ async fn token_login(
382
387
repo : & mut BoxRepository ,
383
388
clock : & dyn Clock ,
384
389
token : & str ,
390
+ requested_device_id : Option < String > ,
391
+ homeserver : & dyn HomeserverConnection ,
392
+ rng : & mut ( dyn RngCore + Send ) ,
385
393
) -> Result < ( CompatSession , User ) , RouteError > {
386
394
let login = repo
387
395
. compat_sso_login ( )
@@ -390,7 +398,7 @@ async fn token_login(
390
398
. ok_or ( RouteError :: InvalidLoginToken ) ?;
391
399
392
400
let now = clock. now ( ) ;
393
- let session_id = match login. state {
401
+ let browser_session_id = match login. state {
394
402
CompatSsoLoginState :: Pending => {
395
403
tracing:: error!(
396
404
compat_sso_login. id = %login. id,
@@ -400,25 +408,25 @@ async fn token_login(
400
408
}
401
409
CompatSsoLoginState :: Fulfilled {
402
410
fulfilled_at,
403
- session_id ,
411
+ browser_session_id ,
404
412
..
405
413
} => {
406
414
if now > fulfilled_at + Duration :: microseconds ( 30 * 1000 * 1000 ) {
407
415
return Err ( RouteError :: LoginTookTooLong ) ;
408
416
}
409
417
410
- session_id
418
+ browser_session_id
411
419
}
412
420
CompatSsoLoginState :: Exchanged {
413
421
exchanged_at,
414
- session_id ,
422
+ compat_session_id ,
415
423
..
416
424
} => {
417
425
if now > exchanged_at + Duration :: microseconds ( 30 * 1000 * 1000 ) {
418
426
// TODO: log that session out
419
427
tracing:: error!(
420
428
compat_sso_login. id = %login. id,
421
- compat_session. id = %session_id ,
429
+ compat_session. id = %compat_session_id ,
422
430
"Login token exchanged a second time more than 30s after"
423
431
) ;
424
432
}
@@ -427,22 +435,56 @@ async fn token_login(
427
435
}
428
436
} ;
429
437
430
- let session = repo
431
- . compat_session ( )
432
- . lookup ( session_id)
433
- . await ?
434
- . ok_or ( RouteError :: SessionNotFound ) ?;
438
+ let Some ( browser_session) = repo. browser_session ( ) . lookup ( browser_session_id) . await ? else {
439
+ tracing:: error!(
440
+ compat_sso_login. id = %login. id,
441
+ browser_session. id = %browser_session_id,
442
+ "Attempt to exchange login token but no associated browser session found"
443
+ ) ;
444
+ return Err ( RouteError :: InvalidLoginToken ) ;
445
+ } ;
446
+ if !browser_session. active ( ) || !browser_session. user . is_valid ( ) {
447
+ tracing:: info!(
448
+ compat_sso_login. id = %login. id,
449
+ browser_session. id = %browser_session_id,
450
+ "Attempt to exchange login token but browser session is not active"
451
+ ) ;
452
+ return Err ( RouteError :: InvalidLoginToken ) ;
453
+ }
435
454
436
- let user = repo
437
- . user ( )
438
- . lookup ( session. user_id )
439
- . await ?
440
- . filter ( mas_data_model:: User :: is_valid)
441
- . ok_or ( RouteError :: UserNotFound ) ?;
455
+ // Lock the user sync to make sure we don't get into a race condition
456
+ repo. user ( )
457
+ . acquire_lock_for_sync ( & browser_session. user )
458
+ . await ?;
442
459
443
- repo. compat_sso_login ( ) . exchange ( clock, login) . await ?;
460
+ let device = if let Some ( requested_device_id) = requested_device_id {
461
+ Device :: from ( requested_device_id)
462
+ } else {
463
+ Device :: generate ( rng)
464
+ } ;
465
+ let mxid = homeserver. mxid ( & browser_session. user . username ) ;
466
+ homeserver
467
+ . create_device ( & mxid, device. as_str ( ) )
468
+ . await
469
+ . map_err ( RouteError :: ProvisionDeviceFailed ) ?;
444
470
445
- Ok ( ( session, user) )
471
+ let compat_session = repo
472
+ . compat_session ( )
473
+ . add (
474
+ rng,
475
+ clock,
476
+ & browser_session. user ,
477
+ device,
478
+ Some ( & browser_session) ,
479
+ false ,
480
+ )
481
+ . await ?;
482
+
483
+ repo. compat_sso_login ( )
484
+ . exchange ( clock, login, & compat_session)
485
+ . await ?;
486
+
487
+ Ok ( ( compat_session, browser_session. user ) )
446
488
}
447
489
448
490
async fn user_password_login (
@@ -1015,7 +1057,7 @@ mod tests {
1015
1057
}
1016
1058
"### ) ;
1017
1059
1018
- let ( device , token) = get_login_token ( & state, & user) . await ;
1060
+ let token = get_login_token ( & state, & user) . await ;
1019
1061
1020
1062
// Try to login with the token.
1021
1063
let request = Request :: post ( "/_matrix/client/v3/login" ) . json ( serde_json:: json!( {
@@ -1026,14 +1068,13 @@ mod tests {
1026
1068
response. assert_status ( StatusCode :: OK ) ;
1027
1069
1028
1070
let body: serde_json:: Value = response. json ( ) ;
1029
- insta:: assert_json_snapshot!( body, @r### "
1071
+ insta:: assert_json_snapshot!( body, @r#"
1030
1072
{
1031
- "access_token": "mct_uihy4bk51gxgUbUTa4XIh92RARTPTj_xADEE4 ",
1032
- "device_id": "Yp7FM44zJN ",
1073
+ "access_token": "mct_bnkWh1tPmm1MZOpygPaXwygX8PfxEY_hE6do1 ",
1074
+ "device_id": "O3Ju1MUh3Z ",
1033
1075
"user_id": "@alice:example.com"
1034
1076
}
1035
- "### ) ;
1036
- assert_eq ! ( body[ "device_id" ] , device. to_string( ) ) ;
1077
+ "# ) ;
1037
1078
1038
1079
// Try again with the same token, it should fail.
1039
1080
let request = Request :: post ( "/_matrix/client/v3/login" ) . json ( serde_json:: json!( {
@@ -1051,7 +1092,7 @@ mod tests {
1051
1092
"### ) ;
1052
1093
1053
1094
// Try to login, but wait too long before sending the request.
1054
- let ( _device , token) = get_login_token ( & state, & user) . await ;
1095
+ let token = get_login_token ( & state, & user) . await ;
1055
1096
1056
1097
// Advance the clock to make the token expire.
1057
1098
state
@@ -1079,14 +1120,13 @@ mod tests {
1079
1120
/// # Panics
1080
1121
///
1081
1122
/// Panics if the repository fails.
1082
- async fn get_login_token ( state : & TestState , user : & User ) -> ( Device , String ) {
1123
+ async fn get_login_token ( state : & TestState , user : & User ) -> String {
1083
1124
// XXX: This is a bit manual, but this is what basically the SSO login flow
1084
1125
// does.
1085
1126
let mut repo = state. repository ( ) . await . unwrap ( ) ;
1086
1127
1087
- // Generate a device and a token randomly
1128
+ // Generate a token randomly
1088
1129
let token = Alphanumeric . sample_string ( & mut state. rng ( ) , 32 ) ;
1089
- let device = Device :: generate ( & mut state. rng ( ) ) ;
1090
1130
1091
1131
// Start a compat SSO login flow
1092
1132
let login = repo
@@ -1100,27 +1140,20 @@ mod tests {
1100
1140
. await
1101
1141
. unwrap ( ) ;
1102
1142
1103
- // Complete the flow by fulfilling it with a session
1104
- let compat_session = repo
1105
- . compat_session ( )
1106
- . add (
1107
- & mut state. rng ( ) ,
1108
- & state. clock ,
1109
- user,
1110
- device. clone ( ) ,
1111
- None ,
1112
- false ,
1113
- )
1143
+ // Advance the flow by fulfilling it with a browser session
1144
+ let browser_session = repo
1145
+ . browser_session ( )
1146
+ . add ( & mut state. rng ( ) , & state. clock , user, None )
1114
1147
. await
1115
1148
. unwrap ( ) ;
1116
-
1117
- repo . compat_sso_login ( )
1118
- . fulfill ( & state. clock , login, & compat_session )
1149
+ let _login = repo
1150
+ . compat_sso_login ( )
1151
+ . fulfill ( & state. clock , login, & browser_session )
1119
1152
. await
1120
1153
. unwrap ( ) ;
1121
1154
1122
1155
repo. save ( ) . await . unwrap ( ) ;
1123
1156
1124
- ( device , token)
1157
+ token
1125
1158
}
1126
1159
}
0 commit comments