Skip to content

Commit df6ef56

Browse files
add sub as userid in cookie
1 parent 5699ff9 commit df6ef56

File tree

5 files changed

+102
-49
lines changed

5 files changed

+102
-49
lines changed

src/handlers/http/oidc.rs

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use ulid::Ulid;
3232
use url::Url;
3333

3434
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},
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+
252266
fn 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+
321343
pub 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?;

src/handlers/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub const TELEMETRY_TYPE_KEY: &str = "x-p-telemetry-type";
3838
const COOKIE_AGE_DAYS: usize = 7;
3939
const SESSION_COOKIE_NAME: &str = "session";
4040
const USER_COOKIE_NAME: &str = "username";
41-
41+
const USER_ID_COOKIE_NAME: &str = "user_id";
4242
// constants for log Source values for known sources and formats
4343
const LOG_SOURCE_KINESIS: &str = "kinesis";
4444

src/rbac/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,10 @@ impl Users {
210210
#[derive(Debug, Serialize, Clone)]
211211
#[serde(rename = "camelCase")]
212212
pub struct UsersPrism {
213-
// username
213+
// sub
214214
pub id: String,
215+
// username
216+
pub username: String,
215217
// oaith or native
216218
pub method: String,
217219
// email only if method is oauth

src/rbac/user.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@ impl User {
6969
pub fn new_oauth(username: String, roles: HashSet<String>, user_info: UserInfo) -> Self {
7070
Self {
7171
ty: UserType::OAuth(OAuth {
72-
userid: user_info.name.clone().unwrap_or(username),
72+
userid: user_info
73+
.sub
74+
.clone()
75+
.or_else(|| user_info.name.clone())
76+
.unwrap_or(username),
7377
user_info,
7478
}),
7579
roles,
@@ -167,6 +171,9 @@ pub struct OAuth {
167171

168172
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
169173
pub struct UserInfo {
174+
#[serde(default)]
175+
/// Subject - Identifier for the End-User at the Issuer.
176+
pub sub: Option<String>,
170177
#[serde(default)]
171178
/// User's full name for display purposes.
172179
pub name: Option<String>,
@@ -185,6 +192,7 @@ pub struct UserInfo {
185192
impl From<openid::Userinfo> for UserInfo {
186193
fn from(user: openid::Userinfo) -> Self {
187194
UserInfo {
195+
sub: user.sub,
188196
name: user.name,
189197
preferred_username: user.preferred_username,
190198
picture: user.picture,

src/rbac/utils.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,19 @@ use super::{
2929
};
3030

3131
pub fn to_prism_user(user: &User) -> UsersPrism {
32-
let (id, method, email, picture) = match &user.ty {
33-
UserType::Native(_) => (user.username(), "native", None, None),
34-
UserType::OAuth(oauth) => (
35-
user.username(),
36-
"oauth",
37-
oauth.user_info.email.clone(),
38-
oauth.user_info.picture.clone(),
39-
),
32+
let (id, username, method, email, picture) = match &user.ty {
33+
UserType::Native(_) => (user.username(), user.username(), "native", None, None),
34+
UserType::OAuth(oauth) => {
35+
let username = user.username();
36+
let display_name = oauth.user_info.name.as_deref().unwrap_or(username);
37+
(
38+
username,
39+
display_name,
40+
"oauth",
41+
oauth.user_info.email.clone(),
42+
oauth.user_info.picture.clone(),
43+
)
44+
}
4045
};
4146
let direct_roles: HashMap<String, Vec<DefaultPrivilege>> = Users
4247
.get_role(id)
@@ -69,6 +74,7 @@ pub fn to_prism_user(user: &User) -> UsersPrism {
6974

7075
UsersPrism {
7176
id: id.into(),
77+
username: username.into(),
7278
method: method.into(),
7379
email: mask_pii_string(email),
7480
picture: mask_pii_url(picture),

0 commit comments

Comments
 (0)