diff --git a/Cargo.lock b/Cargo.lock index 06eacd0..d9812b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/oidc-test/clients-config.json b/oidc-test/clients-config.json new file mode 100644 index 0000000..3268c7b --- /dev/null +++ b/oidc-test/clients-config.json @@ -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" + } + ] + } + ] diff --git a/oidc-test/docker-compose.yml b/oidc-test/docker-compose.yml index e25d18d..2b3a886 100644 --- a/oidc-test/docker-compose.yml +++ b/oidc-test/docker-compose.yml @@ -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: | diff --git a/warpgate-protocol-http/src/api/auth.rs b/warpgate-protocol-http/src/api/auth.rs index e210d54..d1b108e 100644 --- a/warpgate-protocol-http/src/api/auth.rs +++ b/warpgate-protocol-http/src/api/auth.rs @@ -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>>, ) -> poem::Result { - session_middleware.lock().await.remove_session(session); - session.clear(); - info!("Logged out"); + logout(session, session_middleware.lock().await.deref_mut()); Ok(LogoutResponse::Success) } diff --git a/warpgate-protocol-http/src/api/common.rs b/warpgate-protocol-http/src/api/common.rs new file mode 100644 index 0000000..b2976f1 --- /dev/null +++ b/warpgate-protocol-http/src/api/common.rs @@ -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"); +} diff --git a/warpgate-protocol-http/src/api/info.rs b/warpgate-protocol-http/src/api/info.rs index 2dc51d5..a0a2a30 100644 --- a/warpgate-protocol-http/src/api/info.rs +++ b/warpgate-protocol-http/src/api/info.rs @@ -25,6 +25,7 @@ pub struct Info { external_host: Option, 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 { diff --git a/warpgate-protocol-http/src/api/mod.rs b/warpgate-protocol-http/src/api/mod.rs index 1db738c..731cb50 100644 --- a/warpgate-protocol-http/src/api/mod.rs +++ b/warpgate-protocol-http/src/api/mod.rs @@ -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; diff --git a/warpgate-protocol-http/src/api/sso_provider_detail.rs b/warpgate-protocol-http/src/api/sso_provider_detail.rs index 051a92c..3248f1c 100644 --- a/warpgate-protocol-http/src/api/sso_provider_detail.rs +++ b/warpgate-protocol-http/src/api/sso_provider_detail.rs @@ -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, + 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)?, }, ); diff --git a/warpgate-protocol-http/src/api/sso_provider_list.rs b/warpgate-protocol-http/src/api/sso_provider_list.rs index f230995..7037f5d 100644 --- a/warpgate-protocol-http/src/api/sso_provider_list.rs +++ b/warpgate-protocol-http/src/api/sso_provider_list.rs @@ -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, } +#[derive(Object)] +struct StartSloResponseParams { + url: String, +} + +#[allow(clippy::large_enum_variant)] +#[derive(ApiResponse)] +enum StartSloResponse { + #[oai(status = 200)] + Ok(Json), + #[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>>, + ) -> poem::Result { + 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(), + }))) + } } diff --git a/warpgate-protocol-http/src/common.rs b/warpgate-protocol-http/src/common.rs index 168f74a..9298d3c 100644 --- a/warpgate-protocol-http/src/common.rs +++ b/warpgate-protocol-http/src/common.rs @@ -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; 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; fn clear_auth_state(&self); + + fn get_sso_login_state(&self) -> Option; + 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 { + self.get::(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)] diff --git a/warpgate-protocol-http/src/lib.rs b/warpgate-protocol-http/src/lib.rs index 567d312..6a53689 100644 --- a/warpgate-protocol-http/src/lib.rs +++ b/warpgate-protocol-http/src/lib.rs @@ -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}; diff --git a/warpgate-sso/Cargo.toml b/warpgate-sso/Cargo.toml index 5c9262a..25fffa2 100644 --- a/warpgate-sso/Cargo.toml +++ b/warpgate-sso/Cargo.toml @@ -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" diff --git a/warpgate-sso/src/error.rs b/warpgate-sso/src/error.rs index c5a3f91..dbc10bb 100644 --- a/warpgate-sso/src/error.rs +++ b/warpgate-sso/src/error.rs @@ -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), } diff --git a/warpgate-sso/src/lib.rs b/warpgate-sso/src/lib.rs index 59f5951..d92d55e 100644 --- a/warpgate-sso/src/lib.rs +++ b/warpgate-sso/src/lib.rs @@ -9,3 +9,5 @@ pub use error::*; pub use request::*; pub use response::*; pub use sso::*; + +pub use openidconnect::core::CoreIdToken; diff --git a/warpgate-sso/src/request.rs b/warpgate-sso/src/request.rs index d32a750..3d98de0 100644 --- a/warpgate-sso/src/request.rs +++ b/warpgate-sso/src/request.rs @@ -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(), }) } } diff --git a/warpgate-sso/src/response.rs b/warpgate-sso/src/response.rs index e404358..11d666d 100644 --- a/warpgate-sso/src/response.rs +++ b/warpgate-sso/src/response.rs @@ -1,7 +1,10 @@ +use openidconnect::core::CoreIdToken; + #[derive(Clone, Debug)] pub struct SsoLoginResponse { pub name: Option, pub email: Option, pub email_verified: Option, pub groups: Option>, + pub id_token: CoreIdToken, } diff --git a/warpgate-sso/src/sso.rs b/warpgate-sso/src/sso.rs index 67bed66..7b9f7ff 100644 --- a/warpgate-sso/src/sso.rs +++ b/warpgate-sso/src/sso.rs @@ -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 { - let metadata = CoreProviderMetadata::discover_async(config.issuer_url()?, async_http_client) +pub async fn discover_metadata( + config: &SsoInternalProviderConfig, +) -> Result { + 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 { + 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 { + 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 { 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 { + 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()); + } } diff --git a/warpgate-web/src/admin/App.svelte b/warpgate-web/src/admin/App.svelte index e697213..c6eace5 100644 --- a/warpgate-web/src/admin/App.svelte +++ b/warpgate-web/src/admin/App.svelte @@ -1,8 +1,5 @@ + +{#if $serverInfo?.username} +
+ {$serverInfo.username} + {#if $serverInfo.authorizedViaTicket} + (ticket auth) + {/if} +
+ + {#if $serverInfo?.authorizedViaSsoWithSingleLogout} + + + + + + + + Log out of Warpgate + + + + Log out everywhere + + + + {:else} + + {/if} +{/if} diff --git a/warpgate-web/src/gateway/App.svelte b/warpgate-web/src/gateway/App.svelte index 7d32aaf..cf1236a 100644 --- a/warpgate-web/src/gateway/App.svelte +++ b/warpgate-web/src/gateway/App.svelte @@ -1,15 +1,13 @@