mirror of
https://github.com/warp-tech/warpgate.git
synced 2024-09-20 06:46:17 +08:00
Add support for role mappings on custom SSO providers. (#920)
This is done using the `role_mappings` property. Roles to be mapped are gotten from the 'warp_groups` oidc claim: ```sso_providers: - name: custom_sso label: Custom SSO provider: type: custom client_id: <client_id> client_secret: <client_secret> issuer_url: <issuer_url> scopes: ["email", "profile", "openid", "warp_groups"] #warp_groups is scope name to request for my demo case, which adds a "warpgate_groups" claim to the userinfo role_mappings: - ["warpgate:admin", "warpgate:admin"] ``` This maps the `warpgate:admin` group from OIDC to the `warpgate:admin` role. This [video on YouTube](https://youtu.be/XCYSGGCgk9Q) demonstrates the functionality --------- Co-authored-by: Eugene <inbox@null.page>
This commit is contained in:
parent
1395d64eac
commit
916d51a4e8
37
Cargo.lock
generated
37
Cargo.lock
generated
|
@ -1613,9 +1613,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
|||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
|
||||
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
|
@ -1628,9 +1628,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
|
||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
|
@ -1638,15 +1638,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
|
||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
|
||||
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
|
@ -1666,9 +1666,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
|
||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
|
@ -1687,9 +1687,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1698,21 +1698,21 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
|
||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
||||
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
|
||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
|
@ -5644,6 +5644,7 @@ version = "0.9.1"
|
|||
dependencies = [
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"futures",
|
||||
"jsonwebtoken",
|
||||
"once_cell",
|
||||
"openidconnect",
|
||||
|
|
23
tests/oidc-mock/clients-config.json
Normal file
23
tests/oidc-mock/clients-config.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
[
|
||||
{
|
||||
"ClientId": "client-credentials-mock-client",
|
||||
"ClientSecrets": ["client-credentials-mock-client-secret"],
|
||||
"Description": "Client for client credentials flow",
|
||||
"AllowedGrantTypes": ["client_credentials", "authorization_code"],
|
||||
"AllowedScopes": ["openid", "profile", "email", "warpgate-scope"],
|
||||
"RedirectUris": ["https://127.0.0.1:8888/@warpgate/api/sso/return"],
|
||||
"ClientClaimsPrefix": "",
|
||||
"Claims": [
|
||||
{
|
||||
"Type": "string_claim",
|
||||
"Value": "string_claim_value",
|
||||
"ValueType": "string"
|
||||
},
|
||||
{
|
||||
"Type": "json_claim",
|
||||
"Value": "[\"value1\", \"value2\"]",
|
||||
"ValueType": "json"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
73
tests/oidc-mock/docker-compose.yml
Normal file
73
tests/oidc-mock/docker-compose.yml
Normal file
|
@ -0,0 +1,73 @@
|
|||
version: '3'
|
||||
services:
|
||||
oidc-server-mock:
|
||||
container_name: oidc-server-mock
|
||||
image: ghcr.io/soluto/oidc-server-mock:latest
|
||||
ports:
|
||||
- '4011:80'
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Development
|
||||
SERVER_OPTIONS_INLINE: |
|
||||
{
|
||||
"AccessTokenJwtType": "JWT",
|
||||
"Discovery": {
|
||||
"ShowKeySet": true
|
||||
},
|
||||
"Authentication": {
|
||||
"CookieSameSiteMode": "Lax",
|
||||
"CheckSessionCookieSameSiteMode": "Lax"
|
||||
}
|
||||
}
|
||||
LOGIN_OPTIONS_INLINE: |
|
||||
{
|
||||
"AllowRememberLogin": false
|
||||
}
|
||||
LOGOUT_OPTIONS_INLINE: |
|
||||
{
|
||||
"AutomaticRedirectAfterSignOut": true
|
||||
}
|
||||
API_SCOPES_INLINE: |
|
||||
- Name: some-app-scope-1
|
||||
- Name: some-app-scope-2
|
||||
IDENTITY_RESOURCES_INLINE: |
|
||||
- Name: warpgate-scope
|
||||
ClaimTypes:
|
||||
- warpgate_groups
|
||||
# API_RESOURCES_INLINE: |
|
||||
# - Name: wapgate_groups
|
||||
# Scopes:
|
||||
# - warpgate
|
||||
USERS_CONFIGURATION_INLINE: |
|
||||
[
|
||||
{
|
||||
"SubjectId":"1",
|
||||
"Username":"User1",
|
||||
"Password":"pwd",
|
||||
"Claims": [
|
||||
{
|
||||
"Type": "name",
|
||||
"Value": "Sam Tailor",
|
||||
"ValueType": "string"
|
||||
},
|
||||
{
|
||||
"Type": "email",
|
||||
"Value": "sam.tailor@gmail.com",
|
||||
"ValueType": "string"
|
||||
},
|
||||
{
|
||||
"Type": "warpgate_groups",
|
||||
"Value": "[\"qa\", \"unknown\"]",
|
||||
"ValueType": "json"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
CLIENTS_CONFIGURATION_PATH: /tmp/config/clients-config.json
|
||||
ASPNET_SERVICES_OPTIONS_INLINE: |
|
||||
{
|
||||
"ForwardedHeadersOptions": {
|
||||
"ForwardedHeaders" : "All"
|
||||
}
|
||||
}
|
||||
volumes:
|
||||
- .:/tmp/config:ro
|
|
@ -13,8 +13,10 @@ pub enum WarpgateError {
|
|||
InvalidCredentialType,
|
||||
#[error(transparent)]
|
||||
Other(Box<dyn Error + Send + Sync>),
|
||||
#[error("user not found")]
|
||||
UserNotFound,
|
||||
#[error("user {0} not found")]
|
||||
UserNotFound(String),
|
||||
#[error("role {0} not found")]
|
||||
RoleNotFound(String),
|
||||
#[error("failed to parse URL: {0}")]
|
||||
UrlParse(#[from] url::ParseError),
|
||||
#[error("deserialization failed: {0}")]
|
||||
|
|
|
@ -62,7 +62,7 @@ impl AuthStateStore {
|
|||
.get_credential_policy(username, supported_credential_types)
|
||||
.await?;
|
||||
let Some(policy) = policy else {
|
||||
return Err(WarpgateError::UserNotFound);
|
||||
return Err(WarpgateError::UserNotFound(username.into()))
|
||||
};
|
||||
|
||||
let state = AuthState::new(
|
||||
|
|
|
@ -3,7 +3,10 @@ use std::sync::Arc;
|
|||
|
||||
use async_trait::async_trait;
|
||||
use data_encoding::BASE64;
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter, QueryOrder};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::*;
|
||||
use warpgate_common::auth::{
|
||||
|
@ -17,7 +20,7 @@ use warpgate_common::{
|
|||
UserPasswordCredential, UserPublicKeyCredential, UserSsoCredential, UserTotpCredential,
|
||||
WarpgateError,
|
||||
};
|
||||
use warpgate_db_entities::{Role, Target, User};
|
||||
use warpgate_db_entities::{Role, Target, User, UserRoleAssignment};
|
||||
|
||||
use super::ConfigProvider;
|
||||
|
||||
|
@ -294,4 +297,66 @@ impl ConfigProvider for DatabaseConfigProvider {
|
|||
|
||||
Ok(intersect)
|
||||
}
|
||||
|
||||
async fn apply_sso_role_mappings(
|
||||
&mut self,
|
||||
username: &str,
|
||||
managed_role_names: Option<Vec<String>>,
|
||||
assigned_role_names: Vec<String>,
|
||||
) -> Result<(), WarpgateError> {
|
||||
let db = self.db.lock().await;
|
||||
|
||||
let user = User::Entity::find()
|
||||
.filter(User::Column::Username.eq(username))
|
||||
.one(&*db)
|
||||
.await
|
||||
.map_err(WarpgateError::from)?
|
||||
.ok_or_else(|| WarpgateError::UserNotFound(username.into()))?;
|
||||
|
||||
let managed_role_names = match managed_role_names {
|
||||
Some(x) => x,
|
||||
None => Role::Entity::find()
|
||||
.all(&*db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.name)
|
||||
.collect(),
|
||||
};
|
||||
|
||||
for role_name in managed_role_names.into_iter() {
|
||||
let role = Role::Entity::find()
|
||||
.filter(Role::Column::Name.eq(role_name.clone()))
|
||||
.one(&*db)
|
||||
.await
|
||||
.map_err(WarpgateError::from)?
|
||||
.ok_or_else(|| WarpgateError::RoleNotFound(role_name.clone()))?;
|
||||
|
||||
let assignment = UserRoleAssignment::Entity::find()
|
||||
.filter(UserRoleAssignment::Column::UserId.eq(user.id))
|
||||
.filter(UserRoleAssignment::Column::RoleId.eq(role.id))
|
||||
.one(&*db)
|
||||
.await
|
||||
.map_err(WarpgateError::from)?;
|
||||
|
||||
match (assignment, assigned_role_names.contains(&role_name)) {
|
||||
(None, true) => {
|
||||
info!("Adding role {role_name} for user {username} (from SSO)");
|
||||
let values = UserRoleAssignment::ActiveModel {
|
||||
user_id: Set(user.id),
|
||||
role_id: Set(role.id),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
values.insert(&*db).await.map_err(WarpgateError::from)?;
|
||||
}
|
||||
(Some(assignment), false) => {
|
||||
info!("Removing role {role_name} for user {username} (from SSO)");
|
||||
assignment.delete(&*db).await.map_err(WarpgateError::from)?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -287,4 +287,13 @@ impl ConfigProvider for FileConfigProvider {
|
|||
|
||||
Ok(intersect)
|
||||
}
|
||||
|
||||
async fn apply_sso_role_mappings(
|
||||
&mut self,
|
||||
_username: &str,
|
||||
_managed_role_names: Option<Vec<String>>,
|
||||
_assigned_role_names: Vec<String>,
|
||||
) -> Result<(), WarpgateError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,13 @@ pub trait ConfigProvider {
|
|||
client_credential: &AuthCredential,
|
||||
) -> Result<Option<String>, WarpgateError>;
|
||||
|
||||
async fn apply_sso_role_mappings(
|
||||
&mut self,
|
||||
username: &str,
|
||||
managed_role_names: Option<Vec<String>>,
|
||||
active_role_names: Vec<String>,
|
||||
) -> Result<(), WarpgateError>;
|
||||
|
||||
async fn get_credential_policy(
|
||||
&mut self,
|
||||
username: &str,
|
||||
|
|
|
@ -130,7 +130,7 @@ impl Api {
|
|||
)
|
||||
.await
|
||||
{
|
||||
Err(WarpgateError::UserNotFound) => {
|
||||
Err(WarpgateError::UserNotFound(_)) => {
|
||||
return Ok(LoginResponse::Failure(Json(LoginFailureResponse {
|
||||
state: ApiAuthState::Failed,
|
||||
})))
|
||||
|
|
|
@ -173,6 +173,7 @@ impl Api {
|
|||
|
||||
info!("SSO login as {email}");
|
||||
|
||||
let provider = context.provider.clone();
|
||||
let cred = AuthCredential::Sso {
|
||||
provider: context.provider,
|
||||
email: email.clone(),
|
||||
|
@ -210,6 +211,38 @@ impl Api {
|
|||
authorize_session(req, username).await?;
|
||||
}
|
||||
|
||||
let providers_config = services.config.lock().await.store.sso_providers.clone();
|
||||
let mut iter = providers_config.iter();
|
||||
let Some(provider_config) = iter.find(|x| x.name == provider) else {
|
||||
return Ok(Err(format!("No provider matching {provider}")));
|
||||
};
|
||||
|
||||
let mappings = provider_config.provider.role_mappings();
|
||||
if let Some(remote_groups) = response.groups {
|
||||
// If mappings is not set, all groups are subject to sync
|
||||
// and names won't be remapped
|
||||
let managed_role_names = mappings
|
||||
.as_ref()
|
||||
.map(|m| m.iter().map(|x| x.1.clone()).collect::<Vec<_>>());
|
||||
|
||||
let active_role_names: Vec<_> = remote_groups
|
||||
.iter()
|
||||
.filter_map({
|
||||
|r| {
|
||||
if let Some(ref mappings) = mappings {
|
||||
mappings.get(r).cloned()
|
||||
} else {
|
||||
Some(r.clone())
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
debug!("SSO role mappings for {username}: active={active_role_names:?}, managed={managed_role_names:?}");
|
||||
cp.apply_sso_role_mappings(&username, managed_role_names, active_role_names)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(Ok(context
|
||||
.next_url
|
||||
.as_deref()
|
||||
|
|
|
@ -15,3 +15,4 @@ serde_json = "1.0"
|
|||
once_cell = "1.17"
|
||||
jsonwebtoken = "8"
|
||||
data-encoding = "2.3"
|
||||
futures = "0.3.30"
|
||||
|
|
|
@ -59,6 +59,7 @@ pub enum SsoInternalProviderConfig {
|
|||
client_secret: ClientSecret,
|
||||
issuer_url: IssuerUrl,
|
||||
scopes: Vec<String>,
|
||||
role_mappings: Option<HashMap<String, String>>,
|
||||
additional_trusted_audiences: Option<Vec<String>>,
|
||||
},
|
||||
}
|
||||
|
@ -170,39 +171,45 @@ impl SsoInternalProviderConfig {
|
|||
#[inline]
|
||||
pub fn extra_parameters(&self) -> HashMap<String, String> {
|
||||
match self {
|
||||
SsoInternalProviderConfig::Google { .. }
|
||||
| SsoInternalProviderConfig::Custom { .. }
|
||||
| SsoInternalProviderConfig::Azure { .. } => HashMap::new(),
|
||||
SsoInternalProviderConfig::Apple { .. } => {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("response_mode".to_string(), "form_post".to_string());
|
||||
map
|
||||
}
|
||||
_ => HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn auth_type(&self) -> AuthType {
|
||||
#[allow(clippy::match_like_matches_macro)]
|
||||
match self {
|
||||
SsoInternalProviderConfig::Google { .. }
|
||||
| SsoInternalProviderConfig::Custom { .. }
|
||||
| SsoInternalProviderConfig::Azure { .. } => AuthType::BasicAuth,
|
||||
SsoInternalProviderConfig::Apple { .. } => AuthType::RequestBody,
|
||||
_ => AuthType::BasicAuth,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn needs_pkce_verifier(&self) -> bool {
|
||||
#[allow(clippy::match_like_matches_macro)]
|
||||
match self {
|
||||
SsoInternalProviderConfig::Google { .. }
|
||||
| SsoInternalProviderConfig::Custom { .. }
|
||||
| SsoInternalProviderConfig::Azure { .. } => true,
|
||||
SsoInternalProviderConfig::Apple { .. } => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn role_mappings(&self) -> Option<HashMap<String, String>> {
|
||||
#[allow(clippy::match_like_matches_macro)]
|
||||
match self {
|
||||
SsoInternalProviderConfig::Custom { role_mappings, .. } => role_mappings.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn additional_trusted_audiences(&self) -> Option<&Vec<String>> {
|
||||
#[allow(clippy::match_like_matches_macro)]
|
||||
match self {
|
||||
SsoInternalProviderConfig::Custom {
|
||||
additional_trusted_audiences,
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
use futures::future::OptionFuture;
|
||||
use openidconnect::core::CoreGenderClaim;
|
||||
use openidconnect::reqwest::async_http_client;
|
||||
use openidconnect::url::Url;
|
||||
use openidconnect::{
|
||||
AccessTokenHash, AuthorizationCode, CsrfToken, Nonce, OAuth2TokenResponse, PkceCodeVerifier,
|
||||
RedirectUrl, RequestTokenError, TokenResponse,
|
||||
AccessTokenHash, AdditionalClaims, AuthorizationCode, CsrfToken, Nonce, OAuth2TokenResponse,
|
||||
PkceCodeVerifier, RedirectUrl, RequestTokenError, TokenResponse, UserInfoClaims,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
struct WarpgateClaims {
|
||||
// This uses the "warpgate_groups" claim from OIDC
|
||||
warpgate_groups: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl AdditionalClaims for WarpgateClaims {}
|
||||
|
||||
use crate::{make_client, SsoError, SsoInternalProviderConfig, SsoLoginResponse};
|
||||
|
||||
|
@ -55,6 +66,25 @@ impl SsoLoginRequest {
|
|||
let id_token = token_response.id_token().ok_or(SsoError::NotOidc)?;
|
||||
let claims = id_token.claims(&client.id_token_verifier(), &self.nonce)?;
|
||||
|
||||
let user_info_req = client
|
||||
.user_info(token_response.access_token().to_owned(), None)
|
||||
.map_err(|err| {
|
||||
error!("Failed to fetch userinfo: {err:?}");
|
||||
err
|
||||
})
|
||||
.ok();
|
||||
|
||||
let userinfo_claims: Option<UserInfoClaims<WarpgateClaims, CoreGenderClaim>> =
|
||||
OptionFuture::from(user_info_req.map(|req| req.request_async(async_http_client)))
|
||||
.await
|
||||
.and_then(|res| {
|
||||
res.map_err(|err| {
|
||||
error!("Failed to fetch userinfo: {err:?}");
|
||||
err
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
|
||||
if let Some(expected_access_token_hash) = claims.access_token_hash() {
|
||||
let actual_access_token_hash = AccessTokenHash::from_token(
|
||||
token_response.access_token(),
|
||||
|
@ -65,14 +95,30 @@ impl SsoLoginRequest {
|
|||
}
|
||||
}
|
||||
|
||||
debug!("OIDC claims: {:?}", claims);
|
||||
debug!("OIDC userinfo claims: {:?}", userinfo_claims);
|
||||
|
||||
macro_rules! get_claim {
|
||||
($method:ident) => {
|
||||
claims
|
||||
.$method()
|
||||
.or(userinfo_claims.as_ref().and_then(|x| x.$method()))
|
||||
};
|
||||
}
|
||||
|
||||
Ok(SsoLoginResponse {
|
||||
name: claims
|
||||
.name()
|
||||
name: get_claim!(name)
|
||||
.and_then(|x| x.get(None))
|
||||
.map(|x| x.as_str())
|
||||
.map(ToString::to_string),
|
||||
email: claims.email().map(|x| x.as_str()).map(ToString::to_string),
|
||||
email_verified: claims.email_verified(),
|
||||
|
||||
email: get_claim!(email)
|
||||
.map(|x| x.as_str())
|
||||
.map(ToString::to_string),
|
||||
|
||||
email_verified: get_claim!(email_verified),
|
||||
|
||||
groups: userinfo_claims.and_then(|x| x.additional_claims().warpgate_groups.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,4 +3,5 @@ pub struct SsoLoginResponse {
|
|||
pub name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub email_verified: Option<bool>,
|
||||
pub groups: Option<Vec<String>>,
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ $: {
|
|||
bind:value={credential.provider}
|
||||
type="select"
|
||||
>
|
||||
<option value="" selected>Any</option>
|
||||
<option value={null} selected>Any</option>
|
||||
{#each providers as provider}
|
||||
<option value={provider.name}>{provider.label ?? provider.name}</option>
|
||||
{/each}
|
||||
|
|
Loading…
Reference in a new issue