@@ -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 ( ) ) ) ,
@@ -167,7 +168,12 @@ pub async fn reply_login(
167
168
. name
168
169
. clone ( )
169
170
. or_else ( || user_info. email . clone ( ) )
170
- . expect ( "OIDC provider did not return a usable identifier (name or email)" ) ;
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 = user_info
174
+ . sub
175
+ . clone ( )
176
+ . expect ( "OIDC provider did not return a usable identifier (sub)" ) ;
171
177
let user_info: user:: UserInfo = user_info. into ( ) ;
172
178
let group: HashSet < String > = claims
173
179
. other
@@ -186,31 +192,14 @@ pub async fn reply_login(
186
192
}
187
193
}
188
194
189
- /// Attempts to find an existing user by trying both name and email identifiers
190
- /// This handles the case where OIDC provider configuration changes over time:
191
- /// - User was initially created with email as username (when name wasn't provided)
192
- /// - Later OIDC provider starts providing name, but user already exists with email as username
193
- fn find_existing_user ( user_info : & user:: UserInfo ) -> Option < User > {
194
- // Try to find user by name first (current preferred identifier)
195
- if let Some ( name) = & user_info. name {
196
- if let Some ( user) = Users . get_user ( name) {
197
- return Some ( user) ;
198
- }
199
- }
200
-
201
- // If not found by name, try by email (fallback for legacy users)
202
- if let Some ( email) = & user_info. email {
203
- if let Some ( user) = Users . get_user ( email) {
204
- return Some ( user) ;
205
- }
206
- }
207
-
208
- None
209
- }
195
+ let default_role = if let Some ( default_role) = DEFAULT_ROLE . lock ( ) . unwrap ( ) . clone ( ) {
196
+ HashSet :: from ( [ default_role] )
197
+ } else {
198
+ HashSet :: new ( )
199
+ } ;
210
200
211
201
let existing_user = find_existing_user ( & user_info) ;
212
-
213
- let final_roles = match existing_user {
202
+ let mut final_roles = match existing_user {
214
203
Some ( ref user) => {
215
204
// For existing users: keep existing roles + add new valid OIDC roles
216
205
let mut roles = user. roles . clone ( ) ;
@@ -220,20 +209,19 @@ pub async fn reply_login(
220
209
None => {
221
210
// For new users: use valid OIDC roles, fallback to default if none
222
211
if valid_oidc_roles. is_empty ( ) {
223
- if let Some ( default_role) = DEFAULT_ROLE . lock ( ) . unwrap ( ) . clone ( ) {
224
- HashSet :: from ( [ default_role] )
225
- } else {
226
- HashSet :: new ( )
227
- }
212
+ default_role. clone ( )
228
213
} else {
229
214
valid_oidc_roles
230
215
}
231
216
}
232
217
} ;
233
-
218
+ if final_roles. is_empty ( ) {
219
+ // If no roles were found, use the default role
220
+ final_roles. clone_from ( & default_role) ;
221
+ }
234
222
let user = match ( existing_user, final_roles) {
235
223
( Some ( user) , roles) => update_user_if_changed ( user, roles, user_info) . await ?,
236
- ( None , roles) => put_user ( & username , roles, user_info) . await ?,
224
+ ( None , roles) => put_user ( & user_id , roles, user_info) . await ?,
237
225
} ;
238
226
let id = Ulid :: new ( ) ;
239
227
Users . new_session ( & user, SessionKey :: SessionId ( id) ) ;
@@ -245,10 +233,36 @@ pub async fn reply_login(
245
233
246
234
Ok ( redirect_to_client (
247
235
& redirect_url,
248
- [ cookie_session ( id) , cookie_username ( & username) ] ,
236
+ [
237
+ cookie_session ( id) ,
238
+ cookie_username ( & username) ,
239
+ cookie_userid ( & user_id) ,
240
+ ] ,
249
241
) )
250
242
}
251
243
244
+ fn find_existing_user ( user_info : & user:: UserInfo ) -> Option < User > {
245
+ if let Some ( sub) = & user_info. sub {
246
+ if let Some ( user) = Users . get_user ( sub) {
247
+ return Some ( user) ;
248
+ }
249
+ }
250
+
251
+ if let Some ( name) = & user_info. name {
252
+ if let Some ( user) = Users . get_user ( name) {
253
+ return Some ( user) ;
254
+ }
255
+ }
256
+
257
+ if let Some ( email) = & user_info. email {
258
+ if let Some ( user) = Users . get_user ( email) {
259
+ return Some ( user) ;
260
+ }
261
+ }
262
+
263
+ None
264
+ }
265
+
252
266
fn exchange_basic_for_cookie ( user : & User , key : SessionKey ) -> Cookie < ' static > {
253
267
let id = Ulid :: new ( ) ;
254
268
Users . remove_session ( & key) ;
@@ -318,6 +332,14 @@ pub fn cookie_username(username: &str) -> Cookie<'static> {
318
332
. finish ( )
319
333
}
320
334
335
+ pub fn cookie_userid ( user_id : & str ) -> Cookie < ' static > {
336
+ Cookie :: build ( USER_ID_COOKIE_NAME , user_id. to_string ( ) )
337
+ . max_age ( time:: Duration :: days ( COOKIE_AGE_DAYS as i64 ) )
338
+ . same_site ( SameSite :: Strict )
339
+ . path ( "/" )
340
+ . finish ( )
341
+ }
342
+
321
343
pub async fn request_token (
322
344
oidc_client : Arc < DiscoveredClient > ,
323
345
login_query : & Login ,
@@ -365,25 +387,40 @@ pub async fn update_user_if_changed(
365
387
group : HashSet < String > ,
366
388
user_info : user:: UserInfo ,
367
389
) -> Result < User , ObjectStorageError > {
390
+ // Store the old username before modifying the user object
391
+ let old_username = user. username ( ) . to_string ( ) ;
368
392
let User { ty, roles, .. } = & mut user;
369
393
let UserType :: OAuth ( oauth_user) = ty else {
370
394
unreachable ! ( )
371
395
} ;
372
396
373
- // update user only if roles or userinfo has changed
374
- if roles == & group && oauth_user. user_info == user_info {
397
+ // Check if userid needs migration to sub (even if nothing else changed)
398
+ let needs_userid_migration = if let Some ( ref sub) = user_info. sub {
399
+ oauth_user. userid != * sub
400
+ } else {
401
+ false
402
+ } ;
403
+
404
+ // update user only if roles, userinfo has changed, or userid needs migration
405
+ if roles == & group && oauth_user. user_info == user_info && !needs_userid_migration {
375
406
return Ok ( user) ;
376
407
}
377
408
378
- oauth_user. user_info = user_info;
409
+ oauth_user. user_info = user_info. clone ( ) ;
379
410
* roles = group;
380
411
412
+ // Update userid to use sub if available (migration from name-based to sub-based identification)
413
+ if let Some ( ref sub) = user_info. sub {
414
+ oauth_user. userid = sub. clone ( ) ;
415
+ }
416
+
381
417
let mut metadata = get_metadata ( ) . await ?;
382
418
419
+ // Find the user entry using the old username (before migration)
383
420
if let Some ( entry) = metadata
384
421
. users
385
422
. iter_mut ( )
386
- . find ( |x| x. username ( ) == user . username ( ) )
423
+ . find ( |x| x. username ( ) == old_username )
387
424
{
388
425
entry. clone_from ( & user) ;
389
426
put_metadata ( & metadata) . await ?;
0 commit comments