Skip to content

Commit e7d7217

Browse files
fix: use email as fallback if name not present in oidc login (#1399)
1 parent 79dd900 commit e7d7217

File tree

5 files changed

+118
-28
lines changed

5 files changed

+118
-28
lines changed

src/handlers/http/oidc.rs

Lines changed: 91 additions & 17 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())),
@@ -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+
228273
fn 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+
297350
pub 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
}

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)