mirror of
https://github.com/warp-tech/warpgate.git
synced 2025-09-19 13:04:41 +08:00
parent
8d53f7b399
commit
b76872febe
11 changed files with 155 additions and 183 deletions
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue