Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions auth/client/rs/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,35 @@ pub struct OidcConfig {
/// instead of showing the login page.
#[serde(default)]
pub auto_redirect: bool,
/// Claim that holds the user's group memberships (eg `groups`).
/// When set, the user's group memberships are synced on each
/// OIDC login. Empty (default) disables group syncing.
///
/// The claim value may be an array of strings or a single string.
#[serde(default)]
pub groups_claim: String,
/// Claim that signals whether the user is an admin.
/// When set, the user's admin status is synced on each OIDC login.
/// Empty (default) disables admin syncing via claim.
///
/// The claim value may be a boolean, or a string / number
/// interpreted as truthy (`"true"`, `"1"`, non-zero).
#[serde(default)]
pub admin_claim: String,
/// Group whose members are granted admin status.
/// When set, a user is treated as admin if this group is present
/// in their `groups_claim`. Empty (default) disables this.
///
/// Combined with `admin_claim` via OR: admin when either the
/// claim is truthy or the user is a member of this group.
/// Requires `groups_claim` to be configured.
#[serde(default)]
pub admin_group: String,
/// Additional OAuth scopes to request beyond `openid`, `profile`
/// and `email`. Some providers only include the groups claim when
/// its scope (eg `groups`) is explicitly requested.
#[serde(default)]
pub additional_scopes: Vec<String>,
}

impl OidcConfig {
Expand Down Expand Up @@ -126,6 +155,38 @@ mod tests {
let config: OidcConfig = serde_json::from_str(json).unwrap();
assert!(!config.auto_redirect);
}

#[test]
fn test_oidc_config_claim_sync_defaults_disabled() {
// Group / admin syncing is opt-in: defaults are empty.
let config = OidcConfig::default();
assert!(config.groups_claim.is_empty());
assert!(config.admin_claim.is_empty());
assert!(config.admin_group.is_empty());
assert!(config.additional_scopes.is_empty());
}

#[test]
fn test_oidc_config_deserialize_without_claim_sync_fields() {
// Backwards compatibility: old configs without the new
// group / admin claim fields still deserialize.
let json = r#"{"enabled":true,"provider":"https://idp.example.com","client_id":"test-id","client_secret":"s","use_full_email":false,"additional_audiences":[],"auto_redirect":true}"#;
let config: OidcConfig = serde_json::from_str(json).unwrap();
assert!(config.groups_claim.is_empty());
assert!(config.admin_claim.is_empty());
assert!(config.admin_group.is_empty());
assert!(config.additional_scopes.is_empty());
}

#[test]
fn test_oidc_config_deserialize_with_claim_sync_fields() {
let json = r#"{"enabled":true,"provider":"https://idp.example.com","client_id":"test-id","groups_claim":"groups","admin_claim":"komodo_admin","admin_group":"komodo-admins","additional_scopes":["groups"]}"#;
let config: OidcConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.groups_claim, "groups");
assert_eq!(config.admin_claim, "komodo_admin");
assert_eq!(config.admin_group, "komodo-admins");
assert_eq!(config.additional_scopes, vec!["groups".to_string()]);
}
}

impl NamedOauthConfig {
Expand Down
15 changes: 15 additions & 0 deletions auth/server/src/api/oidc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,23 @@ pub async fn oidc_callback<I: AuthImpl>(
)
.await?;

let (groups, is_admin) = provider
.get_groups_and_admin(config, &subject, &token, &nonce)
.await;

let user =
auth.find_user_with_oidc_subject(subject.clone()).await?;

let user_id_or_two_factor = match user {
// Log in existing user
Some(user) => {
auth
.sync_oidc_user_claims(
user.id().to_string(),
groups,
is_admin,
)
.await?;
get_user_id_or_two_factor(&auth, &session, &user).await?
}
// Sign up user
Expand Down Expand Up @@ -273,6 +284,10 @@ pub async fn oidc_callback<I: AuthImpl>(

info!(user_id, username, "New user registration (OIDC)");

auth
.sync_oidc_user_claims(user_id.clone(), groups, is_admin)
.await?;

session.insert_authenticated_user_id(&user_id).await?;

UserIdOrTwoFactor::UserId(user_id)
Expand Down
13 changes: 13 additions & 0 deletions auth/server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,19 @@ pub trait AuthImpl: Send + Sync + 'static {
})
}

/// Sync external OIDC claims onto the user on every login, once the
/// id token has been validated. `groups` come from `groups_claim`,
/// and `is_admin` is `None` when no admin claim / group is
/// configured. Defaults to a no-op.
fn sync_oidc_user_claims(
&self,
_user_id: String,
_groups: Vec<String>,
_is_admin: Option<bool>,
) -> DynFuture<mogh_error::Result<()>> {
Box::pin(async { Ok(()) })
}

// ==============
// = NAMED AUTH =
// ==============
Expand Down
Loading