@@ -32,7 +32,7 @@ use ulid::Ulid;
32
32
use url:: Url ;
33
33
34
34
use crate :: {
35
- handlers:: { COOKIE_AGE_DAYS , SESSION_COOKIE_NAME , USER_COOKIE_NAME } ,
35
+ handlers:: { COOKIE_AGE_DAYS , SESSION_COOKIE_NAME , USER_COOKIE_NAME , USER_ID_COOKIE_NAME } ,
36
36
oidc:: { Claims , DiscoveredClient } ,
37
37
parseable:: PARSEABLE ,
38
38
rbac:: {
@@ -102,11 +102,12 @@ pub async fn login(
102
102
} ,
103
103
) if basic. verify_password ( & password) => {
104
104
let user_cookie = cookie_username ( & username) ;
105
+ let user_id_cookie = cookie_userid ( & username) ;
105
106
let session_cookie =
106
107
exchange_basic_for_cookie ( user, SessionKey :: BasicAuth { username, password } ) ;
107
108
Ok ( redirect_to_client (
108
109
query. redirect . as_str ( ) ,
109
- [ user_cookie, session_cookie] ,
110
+ [ user_cookie, user_id_cookie , session_cookie] ,
110
111
) )
111
112
}
112
113
_ => Err ( OIDCError :: BadRequest ( "Bad Request" . to_string ( ) ) ) ,
@@ -166,7 +167,16 @@ pub async fn reply_login(
166
167
let username = user_info
167
168
. name
168
169
. clone ( )
169
- . expect ( "OIDC provider did not return a sub which is currently required." ) ;
170
+ . or_else ( || user_info. email . clone ( ) )
171
+ . or_else ( || user_info. sub . clone ( ) )
172
+ . expect ( "OIDC provider did not return a usable identifier (name, email or sub)" ) ;
173
+ let user_id = match user_info. sub . clone ( ) {
174
+ Some ( id) => id,
175
+ None => {
176
+ tracing:: error!( "OIDC provider did not return a sub" ) ;
177
+ return Err ( OIDCError :: Unauthorized ) ;
178
+ }
179
+ } ;
170
180
let user_info: user:: UserInfo = user_info. into ( ) ;
171
181
let group: HashSet < String > = claims
172
182
. other
@@ -185,8 +195,14 @@ pub async fn reply_login(
185
195
}
186
196
}
187
197
188
- let existing_user = Users . get_user ( & username) ;
189
- let final_roles = match existing_user {
198
+ let default_role = if let Some ( default_role) = DEFAULT_ROLE . lock ( ) . unwrap ( ) . clone ( ) {
199
+ HashSet :: from ( [ default_role] )
200
+ } else {
201
+ HashSet :: new ( )
202
+ } ;
203
+
204
+ let existing_user = find_existing_user ( & user_info) ;
205
+ let mut final_roles = match existing_user {
190
206
Some ( ref user) => {
191
207
// For existing users: keep existing roles + add new valid OIDC roles
192
208
let mut roles = user. roles . clone ( ) ;
@@ -196,20 +212,20 @@ pub async fn reply_login(
196
212
None => {
197
213
// For new users: use valid OIDC roles, fallback to default if none
198
214
if valid_oidc_roles. is_empty ( ) {
199
- if let Some ( default_role) = DEFAULT_ROLE . lock ( ) . unwrap ( ) . clone ( ) {
200
- HashSet :: from ( [ default_role] )
201
- } else {
202
- HashSet :: new ( )
203
- }
215
+ default_role. clone ( )
204
216
} else {
205
217
valid_oidc_roles
206
218
}
207
219
}
208
220
} ;
221
+ if final_roles. is_empty ( ) {
222
+ // If no roles were found, use the default role
223
+ final_roles. clone_from ( & default_role) ;
224
+ }
209
225
210
226
let user = match ( existing_user, final_roles) {
211
227
( Some ( user) , roles) => update_user_if_changed ( user, roles, user_info) . await ?,
212
- ( None , roles) => put_user ( & username , roles, user_info) . await ?,
228
+ ( None , roles) => put_user ( & user_id , roles, user_info) . await ?,
213
229
} ;
214
230
let id = Ulid :: new ( ) ;
215
231
Users . new_session ( & user, SessionKey :: SessionId ( id) ) ;
@@ -221,10 +237,39 @@ pub async fn reply_login(
221
237
222
238
Ok ( redirect_to_client (
223
239
& redirect_url,
224
- [ cookie_session ( id) , cookie_username ( & username) ] ,
240
+ [
241
+ cookie_session ( id) ,
242
+ cookie_username ( & username) ,
243
+ cookie_userid ( & user_id) ,
244
+ ] ,
225
245
) )
226
246
}
227
247
248
+ fn find_existing_user ( user_info : & user:: UserInfo ) -> Option < User > {
249
+ if let Some ( sub) = & user_info. sub
250
+ && let Some ( user) = Users . get_user ( sub)
251
+ && matches ! ( user. ty, UserType :: OAuth ( _) )
252
+ {
253
+ return Some ( user) ;
254
+ }
255
+
256
+ if let Some ( name) = & user_info. name
257
+ && let Some ( user) = Users . get_user ( name)
258
+ && matches ! ( user. ty, UserType :: OAuth ( _) )
259
+ {
260
+ return Some ( user) ;
261
+ }
262
+
263
+ if let Some ( email) = & user_info. email
264
+ && let Some ( user) = Users . get_user ( email)
265
+ && matches ! ( user. ty, UserType :: OAuth ( _) )
266
+ {
267
+ return Some ( user) ;
268
+ }
269
+
270
+ None
271
+ }
272
+
228
273
fn exchange_basic_for_cookie ( user : & User , key : SessionKey ) -> Cookie < ' static > {
229
274
let id = Ulid :: new ( ) ;
230
275
Users . remove_session ( & key) ;
@@ -294,6 +339,14 @@ pub fn cookie_username(username: &str) -> Cookie<'static> {
294
339
. finish ( )
295
340
}
296
341
342
+ pub fn cookie_userid ( user_id : & str ) -> Cookie < ' static > {
343
+ Cookie :: build ( USER_ID_COOKIE_NAME , user_id. to_string ( ) )
344
+ . max_age ( time:: Duration :: days ( COOKIE_AGE_DAYS as i64 ) )
345
+ . same_site ( SameSite :: Strict )
346
+ . path ( "/" )
347
+ . finish ( )
348
+ }
349
+
297
350
pub async fn request_token (
298
351
oidc_client : Arc < DiscoveredClient > ,
299
352
login_query : & Login ,
@@ -341,30 +394,51 @@ pub async fn update_user_if_changed(
341
394
group : HashSet < String > ,
342
395
user_info : user:: UserInfo ,
343
396
) -> Result < User , ObjectStorageError > {
397
+ // Store the old username before modifying the user object
398
+ let old_username = user. username ( ) . to_string ( ) ;
344
399
let User { ty, roles, .. } = & mut user;
345
400
let UserType :: OAuth ( oauth_user) = ty else {
346
401
unreachable ! ( )
347
402
} ;
348
403
349
- // update user only if roles or userinfo has changed
350
- if roles == & group && oauth_user. user_info == user_info {
404
+ // Check if userid needs migration to sub (even if nothing else changed)
405
+ let needs_userid_migration = if let Some ( ref sub) = user_info. sub {
406
+ oauth_user. userid != * sub
407
+ } else {
408
+ false
409
+ } ;
410
+
411
+ // update user only if roles, userinfo has changed, or userid needs migration
412
+ if roles == & group && oauth_user. user_info == user_info && !needs_userid_migration {
351
413
return Ok ( user) ;
352
414
}
353
415
354
- oauth_user. user_info = user_info;
416
+ oauth_user. user_info . clone_from ( & user_info) ;
355
417
* roles = group;
356
418
419
+ // Update userid to use sub if available (migration from name-based to sub-based identification)
420
+ if let Some ( ref sub) = user_info. sub {
421
+ oauth_user. userid . clone_from ( sub) ;
422
+ }
423
+
357
424
let mut metadata = get_metadata ( ) . await ?;
358
425
426
+ // Find the user entry using the old username (before migration)
359
427
if let Some ( entry) = metadata
360
428
. users
361
429
. iter_mut ( )
362
- . find ( |x| x. username ( ) == user . username ( ) )
430
+ . find ( |x| x. username ( ) == old_username )
363
431
{
364
432
entry. clone_from ( & user) ;
433
+ // migrate user references inside user groups
434
+ for group in metadata. user_groups . iter_mut ( ) {
435
+ if group. users . remove ( & old_username) {
436
+ group. users . insert ( user. username ( ) . to_string ( ) ) ;
437
+ }
438
+ }
365
439
put_metadata ( & metadata) . await ?;
366
440
}
367
-
441
+ Users . delete_user ( & old_username ) ;
368
442
Users . put_user ( user. clone ( ) ) ;
369
443
Ok ( user)
370
444
}
0 commit comments