-
-
Notifications
You must be signed in to change notification settings - Fork 150
fix: use email as fallback if name not present in oidc login #1399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c5cee0f
993d5b6
4b271f9
283c3c4
5d3c187
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -32,7 +32,7 @@ use ulid::Ulid; | |||||||||||||||||||||||||||||||
use url::Url; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
use crate::{ | ||||||||||||||||||||||||||||||||
handlers::{COOKIE_AGE_DAYS, SESSION_COOKIE_NAME, USER_COOKIE_NAME}, | ||||||||||||||||||||||||||||||||
handlers::{COOKIE_AGE_DAYS, SESSION_COOKIE_NAME, USER_COOKIE_NAME, USER_ID_COOKIE_NAME}, | ||||||||||||||||||||||||||||||||
oidc::{Claims, DiscoveredClient}, | ||||||||||||||||||||||||||||||||
parseable::PARSEABLE, | ||||||||||||||||||||||||||||||||
rbac::{ | ||||||||||||||||||||||||||||||||
|
@@ -102,11 +102,12 @@ pub async fn login( | |||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||
) if basic.verify_password(&password) => { | ||||||||||||||||||||||||||||||||
let user_cookie = cookie_username(&username); | ||||||||||||||||||||||||||||||||
let user_id_cookie = cookie_userid(&username); | ||||||||||||||||||||||||||||||||
let session_cookie = | ||||||||||||||||||||||||||||||||
exchange_basic_for_cookie(user, SessionKey::BasicAuth { username, password }); | ||||||||||||||||||||||||||||||||
Ok(redirect_to_client( | ||||||||||||||||||||||||||||||||
query.redirect.as_str(), | ||||||||||||||||||||||||||||||||
[user_cookie, session_cookie], | ||||||||||||||||||||||||||||||||
[user_cookie, user_id_cookie, session_cookie], | ||||||||||||||||||||||||||||||||
)) | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
_ => Err(OIDCError::BadRequest("Bad Request".to_string())), | ||||||||||||||||||||||||||||||||
|
@@ -166,7 +167,13 @@ pub async fn reply_login( | |||||||||||||||||||||||||||||||
let username = user_info | ||||||||||||||||||||||||||||||||
.name | ||||||||||||||||||||||||||||||||
.clone() | ||||||||||||||||||||||||||||||||
.expect("OIDC provider did not return a sub which is currently required."); | ||||||||||||||||||||||||||||||||
.or_else(|| user_info.email.clone()) | ||||||||||||||||||||||||||||||||
.or_else(|| user_info.sub.clone()) | ||||||||||||||||||||||||||||||||
.expect("OIDC provider did not return a usable identifier (name, email or sub)"); | ||||||||||||||||||||||||||||||||
let user_id = user_info | ||||||||||||||||||||||||||||||||
.sub | ||||||||||||||||||||||||||||||||
.clone() | ||||||||||||||||||||||||||||||||
.expect("OIDC provider did not return a usable identifier (sub)"); | ||||||||||||||||||||||||||||||||
let user_info: user::UserInfo = user_info.into(); | ||||||||||||||||||||||||||||||||
let group: HashSet<String> = claims | ||||||||||||||||||||||||||||||||
.other | ||||||||||||||||||||||||||||||||
|
@@ -185,8 +192,14 @@ pub async fn reply_login( | |||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
let existing_user = Users.get_user(&username); | ||||||||||||||||||||||||||||||||
let final_roles = match existing_user { | ||||||||||||||||||||||||||||||||
let default_role = if let Some(default_role) = DEFAULT_ROLE.lock().unwrap().clone() { | ||||||||||||||||||||||||||||||||
HashSet::from([default_role]) | ||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||
HashSet::new() | ||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
let existing_user = find_existing_user(&user_info); | ||||||||||||||||||||||||||||||||
let mut final_roles = match existing_user { | ||||||||||||||||||||||||||||||||
Some(ref user) => { | ||||||||||||||||||||||||||||||||
// For existing users: keep existing roles + add new valid OIDC roles | ||||||||||||||||||||||||||||||||
let mut roles = user.roles.clone(); | ||||||||||||||||||||||||||||||||
|
@@ -196,20 +209,19 @@ pub async fn reply_login( | |||||||||||||||||||||||||||||||
None => { | ||||||||||||||||||||||||||||||||
// For new users: use valid OIDC roles, fallback to default if none | ||||||||||||||||||||||||||||||||
if valid_oidc_roles.is_empty() { | ||||||||||||||||||||||||||||||||
if let Some(default_role) = DEFAULT_ROLE.lock().unwrap().clone() { | ||||||||||||||||||||||||||||||||
HashSet::from([default_role]) | ||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||
HashSet::new() | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
default_role.clone() | ||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||
valid_oidc_roles | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
if final_roles.is_empty() { | ||||||||||||||||||||||||||||||||
// If no roles were found, use the default role | ||||||||||||||||||||||||||||||||
final_roles.clone_from(&default_role); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
let user = match (existing_user, final_roles) { | ||||||||||||||||||||||||||||||||
(Some(user), roles) => update_user_if_changed(user, roles, user_info).await?, | ||||||||||||||||||||||||||||||||
(None, roles) => put_user(&username, roles, user_info).await?, | ||||||||||||||||||||||||||||||||
(None, roles) => put_user(&user_id, roles, user_info).await?, | ||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||
let id = Ulid::new(); | ||||||||||||||||||||||||||||||||
Users.new_session(&user, SessionKey::SessionId(id)); | ||||||||||||||||||||||||||||||||
|
@@ -221,10 +233,36 @@ pub async fn reply_login( | |||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
Ok(redirect_to_client( | ||||||||||||||||||||||||||||||||
&redirect_url, | ||||||||||||||||||||||||||||||||
[cookie_session(id), cookie_username(&username)], | ||||||||||||||||||||||||||||||||
[ | ||||||||||||||||||||||||||||||||
cookie_session(id), | ||||||||||||||||||||||||||||||||
cookie_username(&username), | ||||||||||||||||||||||||||||||||
cookie_userid(&user_id), | ||||||||||||||||||||||||||||||||
], | ||||||||||||||||||||||||||||||||
)) | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
fn find_existing_user(user_info: &user::UserInfo) -> Option<User> { | ||||||||||||||||||||||||||||||||
if let Some(sub) = &user_info.sub | ||||||||||||||||||||||||||||||||
&& let Some(user) = Users.get_user(sub) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
return Some(user); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
if let Some(name) = &user_info.name | ||||||||||||||||||||||||||||||||
&& let Some(user) = Users.get_user(name) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
return Some(user); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
if let Some(email) = &user_info.email | ||||||||||||||||||||||||||||||||
&& let Some(user) = Users.get_user(email) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
return Some(user); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
None | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
fn exchange_basic_for_cookie(user: &User, key: SessionKey) -> Cookie<'static> { | ||||||||||||||||||||||||||||||||
let id = Ulid::new(); | ||||||||||||||||||||||||||||||||
Users.remove_session(&key); | ||||||||||||||||||||||||||||||||
|
@@ -294,6 +332,14 @@ pub fn cookie_username(username: &str) -> Cookie<'static> { | |||||||||||||||||||||||||||||||
.finish() | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
pub fn cookie_userid(user_id: &str) -> Cookie<'static> { | ||||||||||||||||||||||||||||||||
Cookie::build(USER_ID_COOKIE_NAME, user_id.to_string()) | ||||||||||||||||||||||||||||||||
.max_age(time::Duration::days(COOKIE_AGE_DAYS as i64)) | ||||||||||||||||||||||||||||||||
.same_site(SameSite::Strict) | ||||||||||||||||||||||||||||||||
.path("/") | ||||||||||||||||||||||||||||||||
.finish() | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
Comment on lines
+335
to
+341
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Harden cookies: set Session/user cookies should be pub fn cookie_userid(user_id: &str) -> Cookie<'static> {
Cookie::build(USER_ID_COOKIE_NAME, user_id.to_string())
.max_age(time::Duration::days(COOKIE_AGE_DAYS as i64))
+ .secure(true)
.same_site(SameSite::Strict)
.path("/")
.finish()
} Additionally (outside this hunk), consider:
📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
pub async fn request_token( | ||||||||||||||||||||||||||||||||
oidc_client: Arc<DiscoveredClient>, | ||||||||||||||||||||||||||||||||
login_query: &Login, | ||||||||||||||||||||||||||||||||
|
@@ -341,25 +387,40 @@ pub async fn update_user_if_changed( | |||||||||||||||||||||||||||||||
group: HashSet<String>, | ||||||||||||||||||||||||||||||||
user_info: user::UserInfo, | ||||||||||||||||||||||||||||||||
) -> Result<User, ObjectStorageError> { | ||||||||||||||||||||||||||||||||
// Store the old username before modifying the user object | ||||||||||||||||||||||||||||||||
let old_username = user.username().to_string(); | ||||||||||||||||||||||||||||||||
let User { ty, roles, .. } = &mut user; | ||||||||||||||||||||||||||||||||
let UserType::OAuth(oauth_user) = ty else { | ||||||||||||||||||||||||||||||||
unreachable!() | ||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// update user only if roles or userinfo has changed | ||||||||||||||||||||||||||||||||
if roles == &group && oauth_user.user_info == user_info { | ||||||||||||||||||||||||||||||||
// Check if userid needs migration to sub (even if nothing else changed) | ||||||||||||||||||||||||||||||||
let needs_userid_migration = if let Some(ref sub) = user_info.sub { | ||||||||||||||||||||||||||||||||
oauth_user.userid != *sub | ||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||
false | ||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// update user only if roles, userinfo has changed, or userid needs migration | ||||||||||||||||||||||||||||||||
if roles == &group && oauth_user.user_info == user_info && !needs_userid_migration { | ||||||||||||||||||||||||||||||||
return Ok(user); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
oauth_user.user_info = user_info; | ||||||||||||||||||||||||||||||||
oauth_user.user_info.clone_from(&user_info); | ||||||||||||||||||||||||||||||||
*roles = group; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// Update userid to use sub if available (migration from name-based to sub-based identification) | ||||||||||||||||||||||||||||||||
if let Some(ref sub) = user_info.sub { | ||||||||||||||||||||||||||||||||
oauth_user.userid.clone_from(sub); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
let mut metadata = get_metadata().await?; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// Find the user entry using the old username (before migration) | ||||||||||||||||||||||||||||||||
if let Some(entry) = metadata | ||||||||||||||||||||||||||||||||
.users | ||||||||||||||||||||||||||||||||
.iter_mut() | ||||||||||||||||||||||||||||||||
.find(|x| x.username() == user.username()) | ||||||||||||||||||||||||||||||||
.find(|x| x.username() == old_username) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
entry.clone_from(&user); | ||||||||||||||||||||||||||||||||
put_metadata(&metadata).await?; | ||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -29,14 +29,19 @@ use super::{ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pub fn to_prism_user(user: &User) -> UsersPrism { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let (id, method, email, picture) = match &user.ty { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
UserType::Native(_) => (user.username(), "native", None, None), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
UserType::OAuth(oauth) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
user.username(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"oauth", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
oauth.user_info.email.clone(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
oauth.user_info.picture.clone(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let (id, username, method, email, picture) = match &user.ty { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
UserType::Native(_) => (user.username(), user.username(), "native", None, None), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
UserType::OAuth(oauth) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let username = user.username(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let display_name = oauth.user_info.name.as_deref().unwrap_or(username); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
username, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
display_name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"oauth", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
oauth.user_info.email.clone(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
oauth.user_info.picture.clone(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+32
to
45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Use email as display fallback when name is missing (consistent with login cookie) Bring to_prism_user in line with reply_login: prefer name, then email, else id (sub). This avoids showing the opaque sub when name is absent. - UserType::OAuth(oauth) => {
- let username = user.username();
- let display_name = oauth.user_info.name.as_deref().unwrap_or(username);
+ UserType::OAuth(oauth) => {
+ let username = user.username(); // id (sub)
+ let display_name = oauth
+ .user_info
+ .name
+ .as_deref()
+ .or_else(|| oauth.user_info.email.as_deref())
+ .unwrap_or(username);
(
username,
display_name,
"oauth",
oauth.user_info.email.clone(),
oauth.user_info.picture.clone(),
)
} 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let direct_roles: HashMap<String, Vec<DefaultPrivilege>> = Users | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.get_role(id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -69,6 +74,7 @@ pub fn to_prism_user(user: &User) -> UsersPrism { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
UsersPrism { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
id: id.into(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
username: username.into(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
method: method.into(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
email: mask_pii_string(email), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
picture: mask_pii_url(picture), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid panics on missing claims; derive
user_id
robustlyUsing
.expect(...)
will crash the handler if an OIDC provider omits fields in userinfo. The ID Token’ssub
is required; fall back to it and return an error instead of panicking.🤖 Prompt for AI Agents