fixed #952 - auto create sso users (#1245)

This commit is contained in:
Eugene 2025-02-10 01:12:50 +01:00 committed by GitHub
parent 8d53f7b399
commit b76872febe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 155 additions and 183 deletions

View file

@ -1,39 +0,0 @@
[
{
"ClientId": "implicit-mock-client",
"Description": "Client for implicit flow",
"AllowedGrantTypes": ["implicit"],
"AllowAccessTokensViaBrowser": true,
"RedirectUris": [
"https://warpgate.com/@warpgate/api/sso/return",
"https://127.0.0.1:8888/@warpgate/api/sso/return"
],
"AllowedScopes": ["openid", "profile", "email", "warpgate-scope"],
"IdentityTokenLifetime": 3600,
"AccessTokenLifetime": 3600
},
{
"ClientId": "client-credentials-mock-client",
"ClientSecrets": ["client-credentials-mock-client-secret"],
"Description": "Client for client credentials flow",
"AllowedGrantTypes": ["authorization_code"],
"AllowedScopes": ["openid", "profile", "email", "warpgate-scope"],
"ClientClaimsPrefix": "",
"RedirectUris": [
"https://warpgate.com/@warpgate/api/sso/return",
"https://127.0.0.1:8888/@warpgate/api/sso/return"
],
"Claims": [
{
"Type": "string_claim",
"Value": "string_claim_value",
"ValueType": "string"
},
{
"Type": "json_claim",
"Value": "[\"value1\", \"value2\"]",
"ValueType": "json"
}
]
}
]

View file

@ -1,80 +0,0 @@
version: '3'
services:
oidc-server-mock:
container_name: oidc-server-mock
image: ghcr.io/soluto/oidc-server-mock:latest
ports:
- '4011:8080'
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
API_RESOURCES_INLINE: |
- Name: some-app
Scopes:
- some-app-scope-1
- some-app-scope-2
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": "some-api-resource-claim",
"Value": "Sam's Api Resource Custom Claim",
"ValueType": "string"
},
{
"Type": "some-api-scope-claim",
"Value": "Sam's Api Scope Custom Claim",
"ValueType": "string"
},
{
"Type": "some-identity-resource-claim",
"Value": "Sam's Identity Resource Custom Claim",
"ValueType": "string"
}
]
}
]
CLIENTS_CONFIGURATION_PATH: /tmp/config/clients-config.json
ASPNET_SERVICES_OPTIONS_INLINE: |
{
"ForwardedHeadersOptions": {
"ForwardedHeaders" : "All"
}
}
volumes:
- .:/tmp/config:ro

View file

@ -1,23 +1,57 @@
[
{
"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"
}
]
}
]
{
"ClientId": "implicit-mock-client",
"Description": "Client for implicit flow",
"AllowedGrantTypes": [
"implicit"
],
"AllowAccessTokensViaBrowser": true,
"RedirectUris": [
"https://warpgate.com/@warpgate/api/sso/return",
"https://127.0.0.1:8888/@warpgate/api/sso/return"
],
"AllowedScopes": [
"openid",
"profile",
"email",
"warpgate-scope",
"preferred_username"
],
"IdentityTokenLifetime": 3600,
"AccessTokenLifetime": 3600
},
{
"ClientId": "client-credentials-mock-client",
"ClientSecrets": [
"client-credentials-mock-client-secret"
],
"Description": "Client for client credentials flow",
"AllowedGrantTypes": [
"authorization_code"
],
"AllowedScopes": [
"openid",
"profile",
"email",
"warpgate-scope",
"preferred_username"
],
"ClientClaimsPrefix": "",
"RedirectUris": [
"https://warpgate.com/@warpgate/api/sso/return",
"https://127.0.0.1:8888/@warpgate/api/sso/return"
],
"Claims": [
{
"Type": "string_claim",
"Value": "string_claim_value",
"ValueType": "string"
},
{
"Type": "json_claim",
"Value": "[\"value1\", \"value2\"]",
"ValueType": "json"
}
]
}
]

View file

@ -2,9 +2,10 @@ version: '3'
services:
oidc-server-mock:
container_name: oidc-server-mock
image: ghcr.io/soluto/oidc-server-mock:latest
image: ghcr.io/soluto/oidc-server-mock:0.10.1
platform: linux/amd64
ports:
- '4011:80'
- '4011:8080'
environment:
ASPNETCORE_ENVIRONMENT: Development
SERVER_OPTIONS_INLINE: |
@ -20,23 +21,16 @@ services:
}
LOGIN_OPTIONS_INLINE: |
{
"AllowRememberLogin": false
"AllowRememberLogin": true
}
LOGOUT_OPTIONS_INLINE: |
{
"AutomaticRedirectAfterSignOut": true
}
API_SCOPES_INLINE: |
- Name: some-app-scope-1
- Name: some-app-scope-2
IDENTITY_RESOURCES_INLINE: |
- Name: warpgate-scope
- Name: preferred_username
ClaimTypes:
- warpgate_roles
# API_RESOURCES_INLINE: |
# - Name: wapgate_groups
# Scopes:
# - warpgate
- preferred_username
USERS_CONFIGURATION_INLINE: |
[
{
@ -55,10 +49,10 @@ services:
"ValueType": "string"
},
{
"Type": "warpgate_roles",
"Value": "[\"qa\", \"unknown\"]",
"ValueType": "json"
}
"Type": "preferred_username",
"Value": "sam_tailor",
"ValueType": "string"
},
]
}
]

View file

@ -9,6 +9,7 @@ use sea_orm::{
};
use tokio::sync::Mutex;
use tracing::*;
use uuid::Uuid;
use warpgate_common::auth::{
AllCredentialsPolicy, AnySingleCredentialPolicy, AuthCredential, CredentialKind,
CredentialPolicy, PerProtocolCredentialPolicy,
@ -17,9 +18,10 @@ use warpgate_common::helpers::hash::verify_password_hash;
use warpgate_common::helpers::otp::verify_totp;
use warpgate_common::{
Role, Target, User, UserAuthCredential, UserPasswordCredential, UserPublicKeyCredential,
UserSsoCredential, UserTotpCredential, WarpgateError,
UserRequireCredentialsPolicy, UserSsoCredential, UserTotpCredential, WarpgateError,
};
use warpgate_db_entities as entities;
use warpgate_sso::SsoProviderConfig;
use super::ConfigProvider;
@ -31,6 +33,33 @@ impl DatabaseConfigProvider {
pub async fn new(db: &Arc<Mutex<DatabaseConnection>>) -> Self {
Self { db: db.clone() }
}
async fn maybe_autocreate_sso_user(
&self,
db: &DatabaseConnection,
credential: UserSsoCredential,
preferred_username: String,
) -> Result<Option<String>, WarpgateError> {
let user = entities::User::ActiveModel {
id: Set(Uuid::new_v4()),
username: Set(preferred_username.clone()),
credential_policy: Set(serde_json::to_value(
UserRequireCredentialsPolicy::default(),
)?),
}
.insert(&*db)
.await?;
entities::SsoCredential::ActiveModel {
id: Set(Uuid::new_v4()),
user_id: Set(user.id),
..entities::SsoCredential::ActiveModel::from(credential)
}
.insert(&*db)
.await?;
Ok(Some(preferred_username))
}
}
impl ConfigProvider for DatabaseConfigProvider {
@ -143,6 +172,8 @@ impl ConfigProvider for DatabaseConfigProvider {
async fn username_for_sso_credential(
&mut self,
client_credential: &AuthCredential,
preferred_username: Option<String>,
sso_config: SsoProviderConfig,
) -> Result<Option<String>, WarpgateError> {
let db = self.db.lock().await;
@ -154,7 +185,7 @@ impl ConfigProvider for DatabaseConfigProvider {
return Ok(None);
};
let Some(cred) = entities::SsoCredential::Entity::find()
let cred = entities::SsoCredential::Entity::find()
.filter(
entities::SsoCredential::Column::Email.eq(client_email).and(
entities::SsoCredential::Column::Provider
@ -163,14 +194,34 @@ impl ConfigProvider for DatabaseConfigProvider {
),
)
.one(&*db)
.await?
else {
return Ok(None);
};
.await?;
let user = cred.find_related(entities::User::Entity).one(&*db).await?;
if let Some(cred) = cred {
let user = cred.find_related(entities::User::Entity).one(&*db).await?;
Ok(user.map(|x| x.username))
if let Some(user) = user {
return Ok(Some(user.username.clone()));
}
}
if sso_config.auto_create_users {
let Some(preferred_username) = preferred_username else {
error!("The OIDC server did not provide a preferred_username claim for this user");
return Ok(None);
};
return Ok(self
.maybe_autocreate_sso_user(
&*db,
UserSsoCredential {
email: client_email.clone(),
provider: Some(client_provider.clone()),
},
preferred_username,
)
.await?);
}
Ok(None)
}
async fn validate_credential(
@ -330,8 +381,7 @@ impl ConfigProvider for DatabaseConfigProvider {
let user = entities::User::Entity::find()
.filter(entities::User::Column::Username.eq(username))
.one(&*db)
.await
.map_err(WarpgateError::from)?
.await?
.ok_or_else(|| WarpgateError::UserNotFound(username.into()))?;
let managed_role_names = match managed_role_names {
@ -348,16 +398,14 @@ impl ConfigProvider for DatabaseConfigProvider {
let role = entities::Role::Entity::find()
.filter(entities::Role::Column::Name.eq(role_name.clone()))
.one(&*db)
.await
.map_err(WarpgateError::from)?
.await?
.ok_or_else(|| WarpgateError::RoleNotFound(role_name.clone()))?;
let assignment = entities::UserRoleAssignment::Entity::find()
.filter(entities::UserRoleAssignment::Column::UserId.eq(user.id))
.filter(entities::UserRoleAssignment::Column::RoleId.eq(role.id))
.one(&*db)
.await
.map_err(WarpgateError::from)?;
.await?;
match (assignment, assigned_role_names.contains(&role_name)) {
(None, true) => {
@ -368,11 +416,11 @@ impl ConfigProvider for DatabaseConfigProvider {
..Default::default()
};
values.insert(&*db).await.map_err(WarpgateError::from)?;
values.insert(&*db).await?;
}
(Some(assignment), false) => {
info!("Removing role {role_name} for user {username} (from SSO)");
assignment.delete(&*db).await.map_err(WarpgateError::from)?;
assignment.delete(&*db).await?;
}
_ => (),
}

View file

@ -11,6 +11,7 @@ use uuid::Uuid;
use warpgate_common::auth::{AuthCredential, CredentialKind, CredentialPolicy};
use warpgate_common::{Secret, Target, User, WarpgateError};
use warpgate_db_entities::Ticket;
use warpgate_sso::SsoProviderConfig;
#[enum_dispatch]
pub enum ConfigProviderEnum {
@ -33,6 +34,8 @@ pub trait ConfigProvider {
async fn username_for_sso_credential(
&mut self,
client_credential: &AuthCredential,
preferred_username: Option<String>,
sso_config: SsoProviderConfig,
) -> Result<Option<String>, WarpgateError>;
async fn apply_sso_role_mappings(

View file

@ -192,7 +192,12 @@ impl Api {
info!("SSO login as {email}");
let provider = context.provider.clone();
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 == context.provider) else {
return Ok(Err(format!("No provider matching {}", context.provider)));
};
let cred = AuthCredential::Sso {
provider: context.provider.clone(),
email: email.clone(),
@ -202,7 +207,11 @@ impl Api {
.config_provider
.lock()
.await
.username_for_sso_credential(&cred)
.username_for_sso_credential(
&cred,
response.preferred_username,
provider_config.clone(),
)
.await?;
let Some(username) = username else {
return Ok(Err(format!("No user matching {email}")));
@ -239,12 +248,6 @@ impl Api {
});
}
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

View file

@ -22,6 +22,8 @@ pub struct SsoProviderConfig {
pub label: Option<String>,
pub provider: SsoInternalProviderConfig,
pub return_domain_whitelist: Option<Vec<String>>,
#[serde(default)]
pub auto_create_users: bool,
}
impl SsoProviderConfig {

View file

@ -42,6 +42,10 @@ impl SsoLoginRequest {
}
Ok(SsoLoginResponse {
preferred_username: get_claim!(preferred_username)
.map(|x| x.as_str())
.map(ToString::to_string),
name: get_claim!(name)
.and_then(|x| x.get(None))
.map(|x| x.as_str())

View file

@ -7,4 +7,5 @@ pub struct SsoLoginResponse {
pub email_verified: Option<bool>,
pub groups: Option<Vec<String>>,
pub id_token: CoreIdToken,
pub preferred_username: Option<String>,
}

View file

@ -194,8 +194,10 @@
Login
<Fa class="ms-2" fw icon={faArrowRight} />
</AsyncButton>
{/if}
<div class="mt-3"></div>
{#if authState === ApiAuthState.Failed}
<Alert color="danger">Incorrect credentials</Alert>
{/if}
@ -210,7 +212,7 @@
{#if authState === ApiAuthState.SsoNeeded || authState === ApiAuthState.NotStarted || authState === ApiAuthState.Failed}
<Loadable promise={ssoProvidersPromise}>
{#snippet children(ssoProviders)}
<div class="mt-5 sso-buttons">
<div class="mt-3 sso-buttons">
{#each ssoProviders as ssoProvider}
<button
class="btn btn-secondary"