@@ -32,7 +32,7 @@ use ulid::Ulid;
3232use url:: Url ;
3333
3434use 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 } ,
3636 oidc:: { Claims , DiscoveredClient } ,
3737 parseable:: PARSEABLE ,
3838 rbac:: {
@@ -102,11 +102,12 @@ pub async fn login(
102102 } ,
103103 ) if basic. verify_password ( & password) => {
104104 let user_cookie = cookie_username ( & username) ;
105+ let user_id_cookie = cookie_userid ( & username) ;
105106 let session_cookie =
106107 exchange_basic_for_cookie ( user, SessionKey :: BasicAuth { username, password } ) ;
107108 Ok ( redirect_to_client (
108109 query. redirect . as_str ( ) ,
109- [ user_cookie, session_cookie] ,
110+ [ user_cookie, user_id_cookie , session_cookie] ,
110111 ) )
111112 }
112113 _ => Err ( OIDCError :: BadRequest ( "Bad Request" . to_string ( ) ) ) ,
@@ -166,7 +167,16 @@ pub async fn reply_login(
166167 let username = user_info
167168 . name
168169 . 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+ } ;
170180 let user_info: user:: UserInfo = user_info. into ( ) ;
171181 let group: HashSet < String > = claims
172182 . other
@@ -185,8 +195,14 @@ pub async fn reply_login(
185195 }
186196 }
187197
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 {
190206 Some ( ref user) => {
191207 // For existing users: keep existing roles + add new valid OIDC roles
192208 let mut roles = user. roles . clone ( ) ;
@@ -196,20 +212,20 @@ pub async fn reply_login(
196212 None => {
197213 // For new users: use valid OIDC roles, fallback to default if none
198214 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 ( )
204216 } else {
205217 valid_oidc_roles
206218 }
207219 }
208220 } ;
221+ if final_roles. is_empty ( ) {
222+ // If no roles were found, use the default role
223+ final_roles. clone_from ( & default_role) ;
224+ }
209225
210226 let user = match ( existing_user, final_roles) {
211227 ( 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 ?,
213229 } ;
214230 let id = Ulid :: new ( ) ;
215231 Users . new_session ( & user, SessionKey :: SessionId ( id) ) ;
@@ -221,10 +237,39 @@ pub async fn reply_login(
221237
222238 Ok ( redirect_to_client (
223239 & redirect_url,
224- [ cookie_session ( id) , cookie_username ( & username) ] ,
240+ [
241+ cookie_session ( id) ,
242+ cookie_username ( & username) ,
243+ cookie_userid ( & user_id) ,
244+ ] ,
225245 ) )
226246}
227247
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+
228273fn exchange_basic_for_cookie ( user : & User , key : SessionKey ) -> Cookie < ' static > {
229274 let id = Ulid :: new ( ) ;
230275 Users . remove_session ( & key) ;
@@ -294,6 +339,14 @@ pub fn cookie_username(username: &str) -> Cookie<'static> {
294339 . finish ( )
295340}
296341
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+
297350pub async fn request_token (
298351 oidc_client : Arc < DiscoveredClient > ,
299352 login_query : & Login ,
@@ -341,30 +394,51 @@ pub async fn update_user_if_changed(
341394 group : HashSet < String > ,
342395 user_info : user:: UserInfo ,
343396) -> Result < User , ObjectStorageError > {
397+ // Store the old username before modifying the user object
398+ let old_username = user. username ( ) . to_string ( ) ;
344399 let User { ty, roles, .. } = & mut user;
345400 let UserType :: OAuth ( oauth_user) = ty else {
346401 unreachable ! ( )
347402 } ;
348403
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 {
351413 return Ok ( user) ;
352414 }
353415
354- oauth_user. user_info = user_info;
416+ oauth_user. user_info . clone_from ( & user_info) ;
355417 * roles = group;
356418
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+
357424 let mut metadata = get_metadata ( ) . await ?;
358425
426+ // Find the user entry using the old username (before migration)
359427 if let Some ( entry) = metadata
360428 . users
361429 . iter_mut ( )
362- . find ( |x| x. username ( ) == user . username ( ) )
430+ . find ( |x| x. username ( ) == old_username )
363431 {
364432 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+ }
365439 put_metadata ( & metadata) . await ?;
366440 }
367-
441+ Users . delete_user ( & old_username ) ;
368442 Users . put_user ( user. clone ( ) ) ;
369443 Ok ( user)
370444}
0 commit comments