@@ -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 ( ) ) ) ,
@@ -167,7 +168,12 @@ pub async fn reply_login(
167168 . name
168169 . clone ( )
169170 . 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)" ) ;
171177 let user_info: user:: UserInfo = user_info. into ( ) ;
172178 let group: HashSet < String > = claims
173179 . other
@@ -186,31 +192,14 @@ pub async fn reply_login(
186192 }
187193 }
188194
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+ } ;
210200
211201 let existing_user = find_existing_user ( & user_info) ;
212-
213- let final_roles = match existing_user {
202+ let mut final_roles = match existing_user {
214203 Some ( ref user) => {
215204 // For existing users: keep existing roles + add new valid OIDC roles
216205 let mut roles = user. roles . clone ( ) ;
@@ -220,20 +209,19 @@ pub async fn reply_login(
220209 None => {
221210 // For new users: use valid OIDC roles, fallback to default if none
222211 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 ( )
228213 } else {
229214 valid_oidc_roles
230215 }
231216 }
232217 } ;
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+ }
234222 let user = match ( existing_user, final_roles) {
235223 ( 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 ?,
237225 } ;
238226 let id = Ulid :: new ( ) ;
239227 Users . new_session ( & user, SessionKey :: SessionId ( id) ) ;
@@ -245,10 +233,36 @@ pub async fn reply_login(
245233
246234 Ok ( redirect_to_client (
247235 & redirect_url,
248- [ cookie_session ( id) , cookie_username ( & username) ] ,
236+ [
237+ cookie_session ( id) ,
238+ cookie_username ( & username) ,
239+ cookie_userid ( & user_id) ,
240+ ] ,
249241 ) )
250242}
251243
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+
252266fn exchange_basic_for_cookie ( user : & User , key : SessionKey ) -> Cookie < ' static > {
253267 let id = Ulid :: new ( ) ;
254268 Users . remove_session ( & key) ;
@@ -318,6 +332,14 @@ pub fn cookie_username(username: &str) -> Cookie<'static> {
318332 . finish ( )
319333}
320334
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+
321343pub async fn request_token (
322344 oidc_client : Arc < DiscoveredClient > ,
323345 login_query : & Login ,
@@ -365,25 +387,40 @@ pub async fn update_user_if_changed(
365387 group : HashSet < String > ,
366388 user_info : user:: UserInfo ,
367389) -> Result < User , ObjectStorageError > {
390+ // Store the old username before modifying the user object
391+ let old_username = user. username ( ) . to_string ( ) ;
368392 let User { ty, roles, .. } = & mut user;
369393 let UserType :: OAuth ( oauth_user) = ty else {
370394 unreachable ! ( )
371395 } ;
372396
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 {
375406 return Ok ( user) ;
376407 }
377408
378- oauth_user. user_info = user_info;
409+ oauth_user. user_info = user_info. clone ( ) ;
379410 * roles = group;
380411
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+
381417 let mut metadata = get_metadata ( ) . await ?;
382418
419+ // Find the user entry using the old username (before migration)
383420 if let Some ( entry) = metadata
384421 . users
385422 . iter_mut ( )
386- . find ( |x| x. username ( ) == user . username ( ) )
423+ . find ( |x| x. username ( ) == old_username )
387424 {
388425 entry. clone_from ( & user) ;
389426 put_metadata ( & metadata) . await ?;
0 commit comments