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]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.28"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
|
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
@ -1628,9 +1628,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.28"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
|
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
|
@ -1638,15 +1638,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.28"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
|
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
version = "0.3.28"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
|
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
@ -1666,9 +1666,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.28"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
|
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-lite"
|
name = "futures-lite"
|
||||||
|
@ -1687,9 +1687,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.28"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1698,21 +1698,21 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.28"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
|
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.28"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.28"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
|
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
@ -5644,6 +5644,7 @@ version = "0.9.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
|
"futures",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"openidconnect",
|
"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,
|
InvalidCredentialType,
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Other(Box<dyn Error + Send + Sync>),
|
Other(Box<dyn Error + Send + Sync>),
|
||||||
#[error("user not found")]
|
#[error("user {0} not found")]
|
||||||
UserNotFound,
|
UserNotFound(String),
|
||||||
|
#[error("role {0} not found")]
|
||||||
|
RoleNotFound(String),
|
||||||
#[error("failed to parse URL: {0}")]
|
#[error("failed to parse URL: {0}")]
|
||||||
UrlParse(#[from] url::ParseError),
|
UrlParse(#[from] url::ParseError),
|
||||||
#[error("deserialization failed: {0}")]
|
#[error("deserialization failed: {0}")]
|
||||||
|
|
|
@ -62,7 +62,7 @@ impl AuthStateStore {
|
||||||
.get_credential_policy(username, supported_credential_types)
|
.get_credential_policy(username, supported_credential_types)
|
||||||
.await?;
|
.await?;
|
||||||
let Some(policy) = policy else {
|
let Some(policy) = policy else {
|
||||||
return Err(WarpgateError::UserNotFound);
|
return Err(WarpgateError::UserNotFound(username.into()))
|
||||||
};
|
};
|
||||||
|
|
||||||
let state = AuthState::new(
|
let state = AuthState::new(
|
||||||
|
|
|
@ -3,7 +3,10 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use data_encoding::BASE64;
|
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 tokio::sync::Mutex;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
use warpgate_common::auth::{
|
use warpgate_common::auth::{
|
||||||
|
@ -17,7 +20,7 @@ use warpgate_common::{
|
||||||
UserPasswordCredential, UserPublicKeyCredential, UserSsoCredential, UserTotpCredential,
|
UserPasswordCredential, UserPublicKeyCredential, UserSsoCredential, UserTotpCredential,
|
||||||
WarpgateError,
|
WarpgateError,
|
||||||
};
|
};
|
||||||
use warpgate_db_entities::{Role, Target, User};
|
use warpgate_db_entities::{Role, Target, User, UserRoleAssignment};
|
||||||
|
|
||||||
use super::ConfigProvider;
|
use super::ConfigProvider;
|
||||||
|
|
||||||
|
@ -294,4 +297,66 @@ impl ConfigProvider for DatabaseConfigProvider {
|
||||||
|
|
||||||
Ok(intersect)
|
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)
|
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,
|
client_credential: &AuthCredential,
|
||||||
) -> Result<Option<String>, WarpgateError>;
|
) -> 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(
|
async fn get_credential_policy(
|
||||||
&mut self,
|
&mut self,
|
||||||
username: &str,
|
username: &str,
|
||||||
|
|
|
@ -130,7 +130,7 @@ impl Api {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Err(WarpgateError::UserNotFound) => {
|
Err(WarpgateError::UserNotFound(_)) => {
|
||||||
return Ok(LoginResponse::Failure(Json(LoginFailureResponse {
|
return Ok(LoginResponse::Failure(Json(LoginFailureResponse {
|
||||||
state: ApiAuthState::Failed,
|
state: ApiAuthState::Failed,
|
||||||
})))
|
})))
|
||||||
|
|
|
@ -173,6 +173,7 @@ impl Api {
|
||||||
|
|
||||||
info!("SSO login as {email}");
|
info!("SSO login as {email}");
|
||||||
|
|
||||||
|
let provider = context.provider.clone();
|
||||||
let cred = AuthCredential::Sso {
|
let cred = AuthCredential::Sso {
|
||||||
provider: context.provider,
|
provider: context.provider,
|
||||||
email: email.clone(),
|
email: email.clone(),
|
||||||
|
@ -210,6 +211,38 @@ impl Api {
|
||||||
authorize_session(req, username).await?;
|
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
|
Ok(Ok(context
|
||||||
.next_url
|
.next_url
|
||||||
.as_deref()
|
.as_deref()
|
||||||
|
|
|
@ -15,3 +15,4 @@ serde_json = "1.0"
|
||||||
once_cell = "1.17"
|
once_cell = "1.17"
|
||||||
jsonwebtoken = "8"
|
jsonwebtoken = "8"
|
||||||
data-encoding = "2.3"
|
data-encoding = "2.3"
|
||||||
|
futures = "0.3.30"
|
||||||
|
|
|
@ -59,6 +59,7 @@ pub enum SsoInternalProviderConfig {
|
||||||
client_secret: ClientSecret,
|
client_secret: ClientSecret,
|
||||||
issuer_url: IssuerUrl,
|
issuer_url: IssuerUrl,
|
||||||
scopes: Vec<String>,
|
scopes: Vec<String>,
|
||||||
|
role_mappings: Option<HashMap<String, String>>,
|
||||||
additional_trusted_audiences: Option<Vec<String>>,
|
additional_trusted_audiences: Option<Vec<String>>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -170,39 +171,45 @@ impl SsoInternalProviderConfig {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn extra_parameters(&self) -> HashMap<String, String> {
|
pub fn extra_parameters(&self) -> HashMap<String, String> {
|
||||||
match self {
|
match self {
|
||||||
SsoInternalProviderConfig::Google { .. }
|
|
||||||
| SsoInternalProviderConfig::Custom { .. }
|
|
||||||
| SsoInternalProviderConfig::Azure { .. } => HashMap::new(),
|
|
||||||
SsoInternalProviderConfig::Apple { .. } => {
|
SsoInternalProviderConfig::Apple { .. } => {
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert("response_mode".to_string(), "form_post".to_string());
|
map.insert("response_mode".to_string(), "form_post".to_string());
|
||||||
map
|
map
|
||||||
}
|
}
|
||||||
|
_ => HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn auth_type(&self) -> AuthType {
|
pub fn auth_type(&self) -> AuthType {
|
||||||
|
#[allow(clippy::match_like_matches_macro)]
|
||||||
match self {
|
match self {
|
||||||
SsoInternalProviderConfig::Google { .. }
|
|
||||||
| SsoInternalProviderConfig::Custom { .. }
|
|
||||||
| SsoInternalProviderConfig::Azure { .. } => AuthType::BasicAuth,
|
|
||||||
SsoInternalProviderConfig::Apple { .. } => AuthType::RequestBody,
|
SsoInternalProviderConfig::Apple { .. } => AuthType::RequestBody,
|
||||||
|
_ => AuthType::BasicAuth,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn needs_pkce_verifier(&self) -> bool {
|
pub fn needs_pkce_verifier(&self) -> bool {
|
||||||
|
#[allow(clippy::match_like_matches_macro)]
|
||||||
match self {
|
match self {
|
||||||
SsoInternalProviderConfig::Google { .. }
|
|
||||||
| SsoInternalProviderConfig::Custom { .. }
|
|
||||||
| SsoInternalProviderConfig::Azure { .. } => true,
|
|
||||||
SsoInternalProviderConfig::Apple { .. } => false,
|
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]
|
#[inline]
|
||||||
pub fn additional_trusted_audiences(&self) -> Option<&Vec<String>> {
|
pub fn additional_trusted_audiences(&self) -> Option<&Vec<String>> {
|
||||||
|
#[allow(clippy::match_like_matches_macro)]
|
||||||
match self {
|
match self {
|
||||||
SsoInternalProviderConfig::Custom {
|
SsoInternalProviderConfig::Custom {
|
||||||
additional_trusted_audiences,
|
additional_trusted_audiences,
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
|
use futures::future::OptionFuture;
|
||||||
|
use openidconnect::core::CoreGenderClaim;
|
||||||
use openidconnect::reqwest::async_http_client;
|
use openidconnect::reqwest::async_http_client;
|
||||||
use openidconnect::url::Url;
|
use openidconnect::url::Url;
|
||||||
use openidconnect::{
|
use openidconnect::{
|
||||||
AccessTokenHash, AuthorizationCode, CsrfToken, Nonce, OAuth2TokenResponse, PkceCodeVerifier,
|
AccessTokenHash, AdditionalClaims, AuthorizationCode, CsrfToken, Nonce, OAuth2TokenResponse,
|
||||||
RedirectUrl, RequestTokenError, TokenResponse,
|
PkceCodeVerifier, RedirectUrl, RequestTokenError, TokenResponse, UserInfoClaims,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
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};
|
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 id_token = token_response.id_token().ok_or(SsoError::NotOidc)?;
|
||||||
let claims = id_token.claims(&client.id_token_verifier(), &self.nonce)?;
|
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() {
|
if let Some(expected_access_token_hash) = claims.access_token_hash() {
|
||||||
let actual_access_token_hash = AccessTokenHash::from_token(
|
let actual_access_token_hash = AccessTokenHash::from_token(
|
||||||
token_response.access_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 {
|
Ok(SsoLoginResponse {
|
||||||
name: claims
|
name: get_claim!(name)
|
||||||
.name()
|
|
||||||
.and_then(|x| x.get(None))
|
.and_then(|x| x.get(None))
|
||||||
.map(|x| x.as_str())
|
.map(|x| x.as_str())
|
||||||
.map(ToString::to_string),
|
.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 name: Option<String>,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub email_verified: Option<bool>,
|
pub email_verified: Option<bool>,
|
||||||
|
pub groups: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,9 +70,9 @@ impl SsoClient {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let (auth_url, csrf_token, nonce) = auth_req.url();
|
let (auth_url, csrf_token, nonce) = auth_req.url();
|
||||||
|
|
||||||
Ok(SsoLoginRequest {
|
Ok(SsoLoginRequest {
|
||||||
auth_url,
|
auth_url,
|
||||||
csrf_token,
|
csrf_token,
|
||||||
|
|
|
@ -113,7 +113,7 @@ $: {
|
||||||
bind:value={credential.provider}
|
bind:value={credential.provider}
|
||||||
type="select"
|
type="select"
|
||||||
>
|
>
|
||||||
<option value="" selected>Any</option>
|
<option value={null} selected>Any</option>
|
||||||
{#each providers as provider}
|
{#each providers as provider}
|
||||||
<option value={provider.name}>{provider.label ?? provider.name}</option>
|
<option value={provider.name}>{provider.label ?? provider.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
Loading…
Reference in a new issue