OIDC RP-initiated logout (SSO single logout) support (#992)

Fixes #935
This commit is contained in:
Eugene 2024-09-10 23:16:42 +02:00 committed by GitHub
parent 116bf9fd4d
commit fe521f2a39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 375 additions and 94 deletions

101
Cargo.lock generated
View file

@ -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"

View 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"
}
]
}
]

View file

@ -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: |

View file

@ -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)
}

View 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");
}

View file

@ -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 {

View file

@ -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;

View file

@ -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)?,
},
);

View file

@ -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(),
})))
}
}

View file

@ -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)]

View file

@ -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};

View file

@ -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"

View file

@ -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>),
}

View file

@ -9,3 +9,5 @@ pub use error::*;
pub use request::*;
pub use response::*;
pub use sso::*;
pub use openidconnect::core::CoreIdToken;

View file

@ -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(),
})
}
}

View file

@ -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,
}

View file

@ -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());
}
}

View file

@ -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}/>

View file

@ -2,7 +2,7 @@
"openapi": "3.0.0",
"info": {
"title": "Warpgate Web Admin",
"version": "0.9.1"
"version": "0.10.2"
},
"servers": [
{

View 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}

View file

@ -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>

View file

@ -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": [

View file

@ -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";

View file

@ -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;