mirror of
https://github.com/warp-tech/warpgate.git
synced 2024-09-20 06:46:17 +08:00
parent
116bf9fd4d
commit
fe521f2a39
101
Cargo.lock
generated
101
Cargo.lock
generated
|
@ -499,6 +499,12 @@ version = "0.21.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
|
@ -821,7 +827,7 @@ dependencies = [
|
|||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex 0.7.0",
|
||||
"strsim 0.11.0",
|
||||
"strsim 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1129,16 +1135,6 @@ dependencies = [
|
|||
"syn 2.0.55",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
|
||||
dependencies = [
|
||||
"darling_core 0.13.4",
|
||||
"darling_macro 0.13.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.14.4"
|
||||
|
@ -1150,17 +1146,13 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.13.4"
|
||||
name = "darling"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
|
||||
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 1.0.109",
|
||||
"darling_core 0.20.10",
|
||||
"darling_macro 0.20.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1178,14 +1170,17 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.13.4"
|
||||
name = "darling_core"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
|
||||
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
|
||||
dependencies = [
|
||||
"darling_core 0.13.4",
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.55",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1199,6 +1194,17 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core 0.20.10",
|
||||
"quote",
|
||||
"syn 2.0.55",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.5.0"
|
||||
|
@ -1312,6 +1318,12 @@ version = "0.15.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.16.9"
|
||||
|
@ -2129,6 +2141,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
|||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown 0.12.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2139,6 +2152,7 @@ checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
|||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2776,19 +2790,23 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
|||
|
||||
[[package]]
|
||||
name = "openidconnect"
|
||||
version = "2.5.1"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98dd5b7049bac4fdd2233b8c9767d42c05da8006fdb79cc903258556d2b18009"
|
||||
checksum = "f47e80a9cfae4462dd29c41e987edd228971d6565553fbc14b8a11e666d91590"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"chrono",
|
||||
"dyn-clone",
|
||||
"ed25519-dalek",
|
||||
"hmac",
|
||||
"http",
|
||||
"itertools 0.10.5",
|
||||
"log",
|
||||
"num-bigint",
|
||||
"oauth2",
|
||||
"p256",
|
||||
"p384",
|
||||
"rand",
|
||||
"ring 0.16.20",
|
||||
"rsa",
|
||||
"serde",
|
||||
"serde-value",
|
||||
"serde_derive",
|
||||
|
@ -2796,6 +2814,7 @@ dependencies = [
|
|||
"serde_path_to_error",
|
||||
"serde_plain",
|
||||
"serde_with",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"thiserror",
|
||||
"url",
|
||||
|
@ -4336,24 +4355,32 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "1.14.0"
|
||||
version = "3.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff"
|
||||
checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.2.6",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "1.5.2"
|
||||
version = "3.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
|
||||
checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350"
|
||||
dependencies = [
|
||||
"darling 0.13.4",
|
||||
"darling 0.20.10",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.55",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4830,9 +4857,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
|||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
|
|
39
oidc-test/clients-config.json
Normal file
39
oidc-test/clients-config.json
Normal file
|
@ -0,0 +1,39 @@
|
|||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -4,7 +4,7 @@ services:
|
|||
container_name: oidc-server-mock
|
||||
image: ghcr.io/soluto/oidc-server-mock:latest
|
||||
ports:
|
||||
- '4011:80'
|
||||
- '4011:8080'
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Development
|
||||
SERVER_OPTIONS_INLINE: |
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::ops::DerefMut;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
@ -14,6 +15,7 @@ use warpgate_common::auth::{AuthCredential, AuthResult, AuthState, CredentialKin
|
|||
use warpgate_common::{Secret, WarpgateError};
|
||||
use warpgate_core::Services;
|
||||
|
||||
use super::common::logout;
|
||||
use crate::common::{
|
||||
authorize_session, endpoint_auth, get_auth_state_for_request, SessionAuthorization, SessionExt,
|
||||
};
|
||||
|
@ -209,9 +211,7 @@ impl Api {
|
|||
session: &Session,
|
||||
session_middleware: Data<&Arc<Mutex<SessionStore>>>,
|
||||
) -> poem::Result<LogoutResponse> {
|
||||
session_middleware.lock().await.remove_session(session);
|
||||
session.clear();
|
||||
info!("Logged out");
|
||||
logout(session, session_middleware.lock().await.deref_mut());
|
||||
Ok(LogoutResponse::Success)
|
||||
}
|
||||
|
||||
|
|
10
warpgate-protocol-http/src/api/common.rs
Normal file
10
warpgate-protocol-http/src/api/common.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
use poem::session::Session;
|
||||
use tracing::info;
|
||||
|
||||
use crate::session::SessionStore;
|
||||
|
||||
pub fn logout(session: &Session, session_middleware: &mut SessionStore) {
|
||||
session_middleware.remove_session(session);
|
||||
session.clear();
|
||||
info!("Logged out");
|
||||
}
|
|
@ -25,6 +25,7 @@ pub struct Info {
|
|||
external_host: Option<String>,
|
||||
ports: PortsInfo,
|
||||
authorized_via_ticket: bool,
|
||||
authorized_via_sso_with_single_logout: bool,
|
||||
}
|
||||
|
||||
#[derive(ApiResponse)]
|
||||
|
@ -64,6 +65,9 @@ impl Api {
|
|||
session.get_auth(),
|
||||
Some(SessionAuthorization::Ticket { .. })
|
||||
),
|
||||
authorized_via_sso_with_single_logout: session
|
||||
.get_sso_login_state()
|
||||
.map_or(false, |state| state.supports_single_logout),
|
||||
ports: if session.is_authenticated() {
|
||||
PortsInfo {
|
||||
ssh: if config.store.ssh.enable {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use poem_openapi::OpenApi;
|
||||
|
||||
pub mod auth;
|
||||
mod common;
|
||||
pub mod info;
|
||||
pub mod sso_provider_detail;
|
||||
pub mod sso_provider_list;
|
||||
|
|
|
@ -5,6 +5,7 @@ use poem_openapi::param::{Path, Query};
|
|||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::{ApiResponse, Object, OpenApi};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::*;
|
||||
use warpgate_core::Services;
|
||||
use warpgate_sso::{SsoClient, SsoLoginRequest};
|
||||
|
||||
|
@ -31,6 +32,7 @@ pub struct SsoContext {
|
|||
pub provider: String,
|
||||
pub request: SsoLoginRequest,
|
||||
pub next_url: Option<String>,
|
||||
pub supports_single_logout: bool,
|
||||
}
|
||||
|
||||
#[OpenApi]
|
||||
|
@ -54,6 +56,7 @@ impl Api {
|
|||
|
||||
let mut return_url = config.construct_external_url(Some(req))?;
|
||||
return_url.set_path("@warpgate/api/sso/return");
|
||||
debug!("Return URL: {}", &return_url);
|
||||
|
||||
let Some(provider_config) = config.store.sso_providers.iter().find(|p| p.name == *name)
|
||||
else {
|
||||
|
@ -74,6 +77,10 @@ impl Api {
|
|||
provider: name,
|
||||
request: sso_req,
|
||||
next_url: next.0.clone(),
|
||||
supports_single_logout: client
|
||||
.supports_single_logout()
|
||||
.await
|
||||
.map_err(poem::error::InternalServerError)?,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
use std::ops::DerefMut;
|
||||
use std::sync::Arc;
|
||||
|
||||
use poem::session::Session;
|
||||
use poem::web::{Data, Form};
|
||||
use poem::Request;
|
||||
|
@ -5,13 +8,17 @@ use poem_openapi::param::Query;
|
|||
use poem_openapi::payload::{Html, Json, Response};
|
||||
use poem_openapi::{ApiResponse, Enum, Object, OpenApi};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::*;
|
||||
use warpgate_common::auth::{AuthCredential, AuthResult};
|
||||
use warpgate_core::Services;
|
||||
use warpgate_sso::SsoInternalProviderConfig;
|
||||
use warpgate_sso::{SsoClient, SsoInternalProviderConfig};
|
||||
|
||||
use super::sso_provider_detail::{SsoContext, SSO_CONTEXT_SESSION_KEY};
|
||||
use crate::common::{authorize_session, get_auth_state_for_request};
|
||||
use crate::api::common::logout;
|
||||
use crate::common::{authorize_session, get_auth_state_for_request, SessionExt};
|
||||
use crate::session::SessionStore;
|
||||
use crate::SsoLoginState;
|
||||
|
||||
pub struct Api;
|
||||
|
||||
|
@ -55,6 +62,22 @@ pub struct ReturnToSsoFormData {
|
|||
pub code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Object)]
|
||||
struct StartSloResponseParams {
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(ApiResponse)]
|
||||
enum StartSloResponse {
|
||||
#[oai(status = 200)]
|
||||
Ok(Json<StartSloResponseParams>),
|
||||
#[oai(status = 400)]
|
||||
NotInSsoSession,
|
||||
#[oai(status = 404)]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
fn make_redirect_url(err: &str) -> String {
|
||||
error!("SSO error: {err}");
|
||||
format!("/@warpgate?login_error={err}")
|
||||
|
@ -175,7 +198,7 @@ impl Api {
|
|||
|
||||
let provider = context.provider.clone();
|
||||
let cred = AuthCredential::Sso {
|
||||
provider: context.provider,
|
||||
provider: context.provider.clone(),
|
||||
email: email.clone(),
|
||||
};
|
||||
|
||||
|
@ -204,11 +227,20 @@ impl Api {
|
|||
|
||||
if cp.validate_credential(&username, &cred).await? {
|
||||
state.add_valid_credential(cred);
|
||||
} else {
|
||||
return Ok(Err(format!(
|
||||
"Failed to validate SSO credential for {username}"
|
||||
)));
|
||||
}
|
||||
|
||||
if let AuthResult::Accepted { username } = state.verify() {
|
||||
auth_state_store.complete(state.id()).await;
|
||||
authorize_session(req, username).await?;
|
||||
session.set_sso_login_state(SsoLoginState {
|
||||
provider: context.provider,
|
||||
token: response.id_token,
|
||||
supports_single_logout: context.supports_single_logout,
|
||||
});
|
||||
}
|
||||
|
||||
let providers_config = services.config.lock().await.store.sso_providers.clone();
|
||||
|
@ -249,4 +281,47 @@ impl Api {
|
|||
.unwrap_or("/@warpgate#/login")
|
||||
.to_owned()))
|
||||
}
|
||||
|
||||
#[oai(
|
||||
path = "/sso/logout",
|
||||
method = "get",
|
||||
operation_id = "initiate_sso_logout"
|
||||
)]
|
||||
async fn api_start_slo(
|
||||
&self,
|
||||
req: &Request,
|
||||
session: &Session,
|
||||
services: Data<&Services>,
|
||||
session_middleware: Data<&Arc<Mutex<SessionStore>>>,
|
||||
) -> poem::Result<StartSloResponse> {
|
||||
let Some(state) = session.get_sso_login_state() else {
|
||||
return Ok(StartSloResponse::NotInSsoSession);
|
||||
};
|
||||
|
||||
let config = services.config.lock().await;
|
||||
|
||||
let return_url = config.construct_external_url(Some(req))?;
|
||||
debug!("Return URL: {}", &return_url);
|
||||
|
||||
let Some(provider_config) = config
|
||||
.store
|
||||
.sso_providers
|
||||
.iter()
|
||||
.find(|p| p.name == state.provider)
|
||||
else {
|
||||
return Ok(StartSloResponse::NotFound);
|
||||
};
|
||||
|
||||
let client = SsoClient::new(provider_config.provider.clone());
|
||||
let logout_url = client
|
||||
.logout(state.token, return_url)
|
||||
.await
|
||||
.map_err(poem::error::InternalServerError)?;
|
||||
|
||||
logout(session, session_middleware.lock().await.deref_mut());
|
||||
|
||||
Ok(StartSloResponse::Ok(Json(StartSloResponseParams {
|
||||
url: logout_url.to_string(),
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ use uuid::Uuid;
|
|||
use warpgate_common::auth::{AuthState, CredentialKind};
|
||||
use warpgate_common::{ProtocolName, TargetOptions, WarpgateError};
|
||||
use warpgate_core::{AuthStateStore, Services};
|
||||
use warpgate_sso::CoreIdToken;
|
||||
|
||||
use crate::session::SessionStore;
|
||||
|
||||
|
@ -18,8 +19,16 @@ pub const PROTOCOL_NAME: ProtocolName = "HTTP";
|
|||
static TARGET_SESSION_KEY: &str = "target_name";
|
||||
static AUTH_SESSION_KEY: &str = "auth";
|
||||
static AUTH_STATE_ID_SESSION_KEY: &str = "auth_state_id";
|
||||
static AUTH_SSO_LOGIN_STATE: &str = "auth_sso_login_state";
|
||||
pub static SESSION_COOKIE_NAME: &str = "warpgate-http-session";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SsoLoginState {
|
||||
pub token: CoreIdToken,
|
||||
pub provider: String,
|
||||
pub supports_single_logout: bool,
|
||||
}
|
||||
|
||||
pub trait SessionExt {
|
||||
fn get_target_name(&self) -> Option<String>;
|
||||
fn set_target_name(&self, target_name: String);
|
||||
|
@ -29,6 +38,9 @@ pub trait SessionExt {
|
|||
fn set_auth(&self, auth: SessionAuthorization);
|
||||
fn get_auth_state_id(&self) -> Option<AuthStateId>;
|
||||
fn clear_auth_state(&self);
|
||||
|
||||
fn get_sso_login_state(&self) -> Option<SsoLoginState>;
|
||||
fn set_sso_login_state(&self, token: SsoLoginState);
|
||||
}
|
||||
|
||||
impl SessionExt for Session {
|
||||
|
@ -63,6 +75,17 @@ impl SessionExt for Session {
|
|||
fn clear_auth_state(&self) {
|
||||
self.remove(AUTH_STATE_ID_SESSION_KEY)
|
||||
}
|
||||
|
||||
fn get_sso_login_state(&self) -> Option<SsoLoginState> {
|
||||
self.get::<String>(AUTH_SSO_LOGIN_STATE)
|
||||
.and_then(|x| serde_json::from_str(&x).ok())
|
||||
}
|
||||
|
||||
fn set_sso_login_state(&self, state: SsoLoginState) {
|
||||
if let Ok(json) = serde_json::to_string(&state) {
|
||||
self.set(AUTH_SSO_LOGIN_STATE, json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
|
|
|
@ -17,7 +17,7 @@ use std::time::Duration;
|
|||
use anyhow::{Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use common::page_admin_auth;
|
||||
pub use common::PROTOCOL_NAME;
|
||||
pub use common::{PROTOCOL_NAME, SsoLoginState};
|
||||
use http::HeaderValue;
|
||||
use logging::{get_client_ip, log_request_result, span_for_request};
|
||||
use poem::endpoint::{EmbeddedFileEndpoint, EmbeddedFilesEndpoint};
|
||||
|
|
|
@ -9,7 +9,7 @@ bytes = "1.3"
|
|||
thiserror = "1.0"
|
||||
tokio = { version = "1.20", features = ["tracing", "macros"] }
|
||||
tracing = "0.1"
|
||||
openidconnect = { version = "2.4", features = ["reqwest", "rustls-tls", "accept-string-booleans"] }
|
||||
openidconnect = { version = "3.5", features = ["reqwest", "rustls-tls", "accept-string-booleans"] }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
once_cell = "1.17"
|
||||
|
|
|
@ -24,6 +24,8 @@ pub enum SsoError {
|
|||
Io(#[from] std::io::Error),
|
||||
#[error("JWT error: {0}")]
|
||||
Jwt(#[from] jsonwebtoken::errors::Error),
|
||||
#[error("the OIDC provider doesn't support RP-initiated logout")]
|
||||
LogoutNotSupported,
|
||||
#[error(transparent)]
|
||||
Other(Box<dyn Error + Send + Sync>),
|
||||
}
|
||||
|
|
|
@ -9,3 +9,5 @@ pub use error::*;
|
|||
pub use request::*;
|
||||
pub use response::*;
|
||||
pub use sso::*;
|
||||
|
||||
pub use openidconnect::core::CoreIdToken;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use futures::future::OptionFuture;
|
||||
use openidconnect::core::CoreGenderClaim;
|
||||
use openidconnect::core::{CoreGenderClaim, CoreIdToken};
|
||||
use openidconnect::reqwest::async_http_client;
|
||||
use openidconnect::url::Url;
|
||||
use openidconnect::{
|
||||
|
@ -63,7 +63,7 @@ impl SsoLoginRequest {
|
|||
e => SsoError::Verification(format!("{e}")),
|
||||
})?;
|
||||
|
||||
let id_token = token_response.id_token().ok_or(SsoError::NotOidc)?;
|
||||
let id_token: &CoreIdToken = token_response.id_token().ok_or(SsoError::NotOidc)?;
|
||||
let claims = id_token.claims(&client.id_token_verifier(), &self.nonce)?;
|
||||
|
||||
let user_info_req = client
|
||||
|
@ -119,6 +119,8 @@ impl SsoLoginRequest {
|
|||
email_verified: get_claim!(email_verified),
|
||||
|
||||
groups: userinfo_claims.and_then(|x| x.additional_claims().warpgate_roles.clone()),
|
||||
|
||||
id_token: id_token.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use openidconnect::core::CoreIdToken;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SsoLoginResponse {
|
||||
pub name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub email_verified: Option<bool>,
|
||||
pub groups: Option<Vec<String>>,
|
||||
pub id_token: CoreIdToken,
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
use std::borrow::Cow;
|
||||
use std::ops::Deref;
|
||||
|
||||
use openidconnect::core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata};
|
||||
use openidconnect::core::{CoreAuthenticationFlow, CoreClient, CoreIdToken};
|
||||
use openidconnect::reqwest::async_http_client;
|
||||
use openidconnect::{CsrfToken, DiscoveryError, Nonce, PkceCodeChallenge, RedirectUrl, Scope};
|
||||
use openidconnect::url::Url;
|
||||
use openidconnect::{
|
||||
CsrfToken, DiscoveryError, LogoutRequest, Nonce, PkceCodeChallenge, PostLogoutRedirectUrl,
|
||||
ProviderMetadataWithLogout, RedirectUrl, Scope,
|
||||
};
|
||||
|
||||
use crate::config::SsoInternalProviderConfig;
|
||||
use crate::request::SsoLoginRequest;
|
||||
|
@ -13,15 +17,21 @@ pub struct SsoClient {
|
|||
config: SsoInternalProviderConfig,
|
||||
}
|
||||
|
||||
pub async fn make_client(config: &SsoInternalProviderConfig) -> Result<CoreClient, SsoError> {
|
||||
let metadata = CoreProviderMetadata::discover_async(config.issuer_url()?, async_http_client)
|
||||
pub async fn discover_metadata(
|
||||
config: &SsoInternalProviderConfig,
|
||||
) -> Result<ProviderMetadataWithLogout, SsoError> {
|
||||
ProviderMetadataWithLogout::discover_async(config.issuer_url()?, async_http_client)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
SsoError::Discovery(match e {
|
||||
DiscoveryError::Request(inner) => format!("Request error: {inner}"),
|
||||
e => format!("{e}"),
|
||||
})
|
||||
})?;
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn make_client(config: &SsoInternalProviderConfig) -> Result<CoreClient, SsoError> {
|
||||
let metadata = discover_metadata(config).await?;
|
||||
|
||||
let client = CoreClient::from_provider_metadata(
|
||||
metadata,
|
||||
|
@ -44,6 +54,14 @@ impl SsoClient {
|
|||
Self { config }
|
||||
}
|
||||
|
||||
pub async fn supports_single_logout(&self) -> Result<bool, SsoError> {
|
||||
let metadata = discover_metadata(&self.config).await?;
|
||||
Ok(metadata
|
||||
.additional_metadata()
|
||||
.end_session_endpoint
|
||||
.is_some())
|
||||
}
|
||||
|
||||
pub async fn start_login(&self, redirect_url: String) -> Result<SsoLoginRequest, SsoError> {
|
||||
let redirect_url = RedirectUrl::new(redirect_url)?;
|
||||
let client = make_client(&self.config).await?;
|
||||
|
@ -82,4 +100,16 @@ impl SsoClient {
|
|||
config: self.config.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn logout(&self, token: CoreIdToken, redirect_url: Url) -> Result<Url, SsoError> {
|
||||
let metadata = discover_metadata(&self.config).await?;
|
||||
let Some(ref url) = metadata.additional_metadata().end_session_endpoint else {
|
||||
return Err(SsoError::LogoutNotSupported);
|
||||
};
|
||||
let mut req: LogoutRequest = url.clone().into();
|
||||
req = req.set_id_token_hint(&token);
|
||||
req = req.set_client_id(self.config.client_id().clone());
|
||||
req = req.set_post_logout_redirect_uri(PostLogoutRedirectUrl::from_url(redirect_url));
|
||||
return Ok(req.http_get_url());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { faSignOut } from '@fortawesome/free-solid-svg-icons'
|
||||
import { api } from 'gateway/lib/api'
|
||||
import { serverInfo, reloadServerInfo } from 'gateway/lib/store'
|
||||
import Fa from 'svelte-fa'
|
||||
|
||||
import Router, { link } from 'svelte-spa-router'
|
||||
import active from 'svelte-spa-router/active'
|
||||
|
@ -10,17 +7,12 @@ import { wrap } from 'svelte-spa-router/wrap'
|
|||
import ThemeSwitcher from 'common/ThemeSwitcher.svelte'
|
||||
import Logo from 'common/Logo.svelte'
|
||||
import DelayedSpinner from 'common/DelayedSpinner.svelte'
|
||||
import AuthBar from 'common/AuthBar.svelte'
|
||||
|
||||
async function init () {
|
||||
await reloadServerInfo()
|
||||
}
|
||||
|
||||
async function logout () {
|
||||
await api.logout()
|
||||
await reloadServerInfo()
|
||||
location.href = '/@warpgate'
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
const routes = {
|
||||
|
@ -86,14 +78,7 @@ const routes = {
|
|||
<a use:link use:active href="/ssh">SSH</a>
|
||||
<a use:link use:active href="/log">Log</a>
|
||||
{/if}
|
||||
{#if $serverInfo?.username}
|
||||
<div class="username ms-auto">
|
||||
{$serverInfo?.username}
|
||||
</div>
|
||||
<button class="btn btn-link" on:click={logout} title="Log out">
|
||||
<Fa icon={faSignOut} fw />
|
||||
</button>
|
||||
{/if}
|
||||
<AuthBar />
|
||||
</header>
|
||||
<main>
|
||||
<Router {routes}/>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Warpgate Web Admin",
|
||||
"version": "0.9.1"
|
||||
"version": "0.10.2"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
|
50
warpgate-web/src/common/AuthBar.svelte
Normal file
50
warpgate-web/src/common/AuthBar.svelte
Normal file
|
@ -0,0 +1,50 @@
|
|||
<script lang="ts">
|
||||
import { faSignOut } from '@fortawesome/free-solid-svg-icons'
|
||||
import Fa from 'svelte-fa'
|
||||
|
||||
import { api } from 'gateway/lib/api'
|
||||
import { serverInfo, reloadServerInfo } from 'gateway/lib/store'
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from '@sveltestrap/sveltestrap'
|
||||
|
||||
async function logout () {
|
||||
await api.logout()
|
||||
await reloadServerInfo()
|
||||
location.href = '/@warpgate'
|
||||
}
|
||||
|
||||
async function singleLogout () {
|
||||
const response = await api.initiateSsoLogout()
|
||||
location.href = response.url
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $serverInfo?.username}
|
||||
<div class="ms-auto">
|
||||
{$serverInfo.username}
|
||||
{#if $serverInfo.authorizedViaTicket}
|
||||
<span class="ml-2">(ticket auth)</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $serverInfo?.authorizedViaSsoWithSingleLogout}
|
||||
<Dropdown>
|
||||
<DropdownToggle color="link" title="Log out options">
|
||||
<Fa icon={faSignOut} fw />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu right={true}>
|
||||
<DropdownItem on:click={logout}>
|
||||
<Fa icon={faSignOut} fw />
|
||||
Log out of Warpgate
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={singleLogout}>
|
||||
<Fa icon={faSignOut} fw />
|
||||
Log out everywhere
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
{:else}
|
||||
<Button color="link" on:click={logout} title="Log out">
|
||||
<Fa icon={faSignOut} fw />
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
|
@ -1,15 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { faSignOut } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Alert } from '@sveltestrap/sveltestrap'
|
||||
import Fa from 'svelte-fa'
|
||||
import Router, { push, type RouteDetail } from 'svelte-spa-router'
|
||||
import { wrap } from 'svelte-spa-router/wrap'
|
||||
import { get } from 'svelte/store'
|
||||
import { api } from 'gateway/lib/api'
|
||||
import { reloadServerInfo, serverInfo } from 'gateway/lib/store'
|
||||
import ThemeSwitcher from 'common/ThemeSwitcher.svelte'
|
||||
import Logo from 'common/Logo.svelte'
|
||||
import DelayedSpinner from 'common/DelayedSpinner.svelte'
|
||||
import AuthBar from 'common/AuthBar.svelte'
|
||||
|
||||
let redirecting = false
|
||||
let serverInfoPromise = reloadServerInfo()
|
||||
|
@ -18,12 +16,6 @@ async function init () {
|
|||
await serverInfoPromise
|
||||
}
|
||||
|
||||
async function logout () {
|
||||
await api.logout()
|
||||
await reloadServerInfo()
|
||||
push('/login')
|
||||
}
|
||||
|
||||
function onPageResume () {
|
||||
redirecting = false
|
||||
init()
|
||||
|
@ -76,17 +68,7 @@ init()
|
|||
<Logo />
|
||||
</a>
|
||||
|
||||
{#if $serverInfo?.username}
|
||||
<div class="ms-auto">
|
||||
{$serverInfo.username}
|
||||
{#if $serverInfo.authorizedViaTicket}
|
||||
<span class="ml-2">(ticket auth)</span>
|
||||
{/if}
|
||||
</div>
|
||||
<button class="btn btn-link" on:click={logout} title="Log out">
|
||||
<Fa icon={faSignOut} fw />
|
||||
</button>
|
||||
{/if}
|
||||
<AuthBar />
|
||||
</div>
|
||||
|
||||
<main>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Warpgate HTTP proxy",
|
||||
"version": "0.9.1"
|
||||
"version": "0.10.2"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
@ -324,6 +324,29 @@
|
|||
"operationId": "return_to_sso_with_form_data"
|
||||
}
|
||||
},
|
||||
"/sso/logout": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json; charset=utf-8": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/StartSloResponseParams"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": ""
|
||||
},
|
||||
"404": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"operationId": "initiate_sso_logout"
|
||||
}
|
||||
},
|
||||
"/sso/providers/{name}/start": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
|
@ -414,7 +437,8 @@
|
|||
"required": [
|
||||
"version",
|
||||
"ports",
|
||||
"authorized_via_ticket"
|
||||
"authorized_via_ticket",
|
||||
"authorized_via_sso_with_single_logout"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
|
@ -434,6 +458,9 @@
|
|||
},
|
||||
"authorized_via_ticket": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"authorized_via_sso_with_single_logout": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -519,6 +546,17 @@
|
|||
"Custom"
|
||||
]
|
||||
},
|
||||
"StartSloResponseParams": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StartSsoResponseParams": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
@import "bootstrap/scss/forms";
|
||||
@import "bootstrap/scss/buttons";
|
||||
@import "bootstrap/scss/transitions";
|
||||
// @import "bootstrap/scss/dropdown";
|
||||
@import "bootstrap/scss/dropdown";
|
||||
// @import "bootstrap/scss/button-group";
|
||||
// @import "bootstrap/scss/nav";
|
||||
// @import "bootstrap/scss/navbar";
|
||||
|
|
|
@ -7,3 +7,4 @@ $pagination-active-bg: transparent;
|
|||
$pagination-hover-bg: transparent;
|
||||
$pagination-focus-bg: transparent;
|
||||
$modal-header-border-color: transparent;
|
||||
$dropdown-link-hover-bg: transparent;
|
||||
|
|
Loading…
Reference in a new issue