From 03d312ca68f1c07950c854d52f0dc9dbd9b7cf52 Mon Sep 17 00:00:00 2001 From: mikoto Date: Wed, 28 Feb 2024 12:30:09 +0000 Subject: [PATCH] finished for real now --- Cargo.lock | 89 +-------------------- Cargo.toml | 2 - src/api/client_server/sso.rs | 70 +++++++++------- src/service/globals/mod.rs | 9 +-- src/service/sso/macaroon.rs | 44 ++++++++++ src/service/sso/mod.rs | 150 +++++++++++------------------------ 6 files changed, 137 insertions(+), 227 deletions(-) create mode 100644 src/service/sso/macaroon.rs diff --git a/Cargo.lock b/Cargo.lock index a1f55d4e..90ee0acb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -540,7 +540,6 @@ dependencies = [ "jsonwebtoken", "lazy_static", "lru-cache", - "macaroon", "nix", "num_cpus", "openidconnect", @@ -845,19 +844,10 @@ dependencies = [ "digest", "elliptic-curve", "rfc6979", - "signature 2.2.0", + "signature", "spki", ] -[[package]] -name = "ed25519" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" -dependencies = [ - "signature 1.6.4", -] - [[package]] name = "ed25519" version = "2.2.3" @@ -865,7 +855,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", - "signature 2.2.0", + "signature", ] [[package]] @@ -875,7 +865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", - "ed25519 2.2.3", + "ed25519", "rand_core", "serde", "sha2", @@ -1678,18 +1668,6 @@ dependencies = [ "zstd-sys", ] -[[package]] -name = "libsodium-sys" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd" -dependencies = [ - "cc", - "libc", - "pkg-config", - "walkdir", -] - [[package]] name = "libsqlite3-sys" version = "0.26.0" @@ -1753,19 +1731,6 @@ dependencies = [ "libc", ] -[[package]] -name = "macaroon" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b15778101dd1d3a58a95fd2af33821b38bc44a408bcf03e0e5e194f42c08050" -dependencies = [ - "base64 0.13.1", - "log", - "serde", - "serde_json", - "sodiumoxide", -] - [[package]] name = "maplit" version = "1.0.2" @@ -2610,7 +2575,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core", - "signature 2.2.0", + "signature", "spki", "subtle", "zeroize", @@ -2907,15 +2872,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "schannel" version = "0.1.23" @@ -3184,12 +3140,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" - [[package]] name = "signature" version = "2.2.0" @@ -3243,18 +3193,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "sodiumoxide" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028" -dependencies = [ - "ed25519 1.5.3", - "libc", - "libsodium-sys", - "serde", -] - [[package]] name = "spin" version = "0.5.2" @@ -3899,16 +3837,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "walkdir" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -4040,15 +3968,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 6d222cb4..d57374bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,8 +55,6 @@ bytes = "1.4.0" http = "0.2.9" # Used to find data directory for default db path directories = "4.0.1" -# Used for SSO authorization -macaroon = "0.3.0" # Used for ruma wrapper serde_json = { version = "1.0.96", features = ["raw_value"] } # Used for appservice registration files diff --git a/src/api/client_server/sso.rs b/src/api/client_server/sso.rs index 7e3c50e3..4bbaaa41 100644 --- a/src/api/client_server/sso.rs +++ b/src/api/client_server/sso.rs @@ -1,20 +1,20 @@ use crate::{ - service::sso::{templates, Provider, COOKIE_STATE_EXPIRATION_SECS}, - services, Error, Ruma, RumaResponse, + service::sso::{macaroon::Macaroon, templates, COOKIE_STATE_EXPIRATION_SECS}, + services, Error, Ruma, }; use askama::Template; use axum::{body::Full, response::IntoResponse}; use axum_extra::extract::cookie::{Cookie, SameSite}; use bytes::BytesMut; -use http::StatusCode; -use macaroon::ByteString; -use openidconnect::{reqwest::{http_client, async_http_client}, AuthorizationCode, CsrfToken, TokenResponse}; +use http::{HeaderValue, StatusCode}; +use openidconnect::{ + AuthorizationCode, CsrfToken, +}; use ruma::api::{ client::{error::ErrorKind, session}, OutgoingResponse, }; use serde::Deserialize; -use time::macros::format_description; /// # `GET /_matrix/client/v3/login/sso/redirect` /// @@ -50,11 +50,13 @@ pub async fn get_sso_redirect_with_provider( let location = Some(body.idp_id.clone()); - let (url, nonce, cookie) = match services().sso.find_one(&body.idp_id).map(|provider| provider.handle_redirect(body.redirect_url.as_deref().unwrap_or_default())) { - Ok(fut)=> fut.await, - Err(e)=> return e.into_response(), - }; - + let (url, nonce, cookie) = + match services().sso.find_one(&body.idp_id).map(|provider| { + provider.handle_redirect(body.redirect_url.unwrap()) + }) { + Ok(fut) => fut.await, + Err(e) => return e.into_response(), + }; let cookie = Cookie::build("openid-state", cookie) .path("/_conduit/client/sso") @@ -107,8 +109,11 @@ fn get_sso_fallback_template(redirect_url: &str) -> axum::response::Response { pub struct Callback { pub code: AuthorizationCode, pub state: CsrfToken, + pub verifier: String, } +pub struct Session {} + /// # `GET /_conduit/client/oidc/callback` /// /// Verify the response received from the identity provider. @@ -117,9 +122,16 @@ pub async fn get_sso_callback( cookie: axum::extract::TypedHeader, axum::extract::Query(callback): axum::extract::Query, ) -> axum::response::Response { - // TODO + let clear_cookie = Cookie::build("openid-state", "") + .path("/_conduit/client/sso") + .finish() + .to_string(); - let Callback { code, state } = callback; + let Callback { + code, + state, + verifier, + } = callback; let Some(cookie) = cookie.get("openid-state") else { return Error::BadRequest( @@ -129,26 +141,24 @@ pub async fn get_sso_callback( .into_response(); }; - let provider = match Provider::verify_macaroon(cookie.as_bytes(), state) - .and_then(|macaroon| services().sso.find_one(macaroon.identifier().into())) - { - Ok(provider) => provider, + let macaroon = match Macaroon::verify(cookie, state.secret()) { + Ok(macaroon) => macaroon, Err(error) => return error.into_response(), }; + let provider = match services().sso.find_one(macaroon.idp_id.as_ref()) { + Ok(provider) => provider, + Err(error) => return error.into_response(), + }; + let session = serde_json::to_string(cookie).unwrap(); + let user_info = provider.handle_callback(code, macaroon.nonce).await; - - let cookie = Cookie::build("openid-state", "") - .path("/_conduit/client/sso") - .finish() - .to_string(); - - let user_info = provider.handle_callback(code, nonce); - - // if let Some(verifier) = pkce { - // macaroon.add_first_party_caveat(format!("verifier = {}", verifier).into()); - // } - - (TypedHeader(ContentType::text_utf8()), "Hello, World!").into_response() + ( + axum::TypedHeader(axum::headers::Location( + HeaderValue::from_str(clear_cookie.as_str()).unwrap(), + )), + "Hello, World!", + ) + .into_response() } diff --git a/src/service/globals/mod.rs b/src/service/globals/mod.rs index 07cd51f6..98ab54de 100644 --- a/src/service/globals/mod.rs +++ b/src/service/globals/mod.rs @@ -75,7 +75,7 @@ pub struct Service { pub rotate: RotationHandler, pub shutdown: AtomicBool, - pub macaroon: Option, + pub macaroon_key: Option, } /// Handles "rotation" of long-polling requests. "Rotation" in this context is similar to "rotation" of log files and the like. @@ -183,11 +183,6 @@ impl Service { // Experimental, partially supported room versions let unstable_room_versions = vec![RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5]; - let macaroon = config - .macaroon_key - .as_ref() - .map(|s| macaroon::MacaroonKey::generate(s.as_bytes())); - let mut s = Self { db, config, @@ -218,7 +213,7 @@ impl Service { sync_receivers: RwLock::new(HashMap::new()), rotate: RotationHandler::new(), shutdown: AtomicBool::new(false), - macaroon, + macaroon_key: config.macaroon_key.clone(), }; fs::create_dir_all(s.get_media_folder())?; diff --git a/src/service/sso/macaroon.rs b/src/service/sso/macaroon.rs new file mode 100644 index 00000000..0c9584f7 --- /dev/null +++ b/src/service/sso/macaroon.rs @@ -0,0 +1,44 @@ +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; +use openidconnect::{Nonce, PkceCodeVerifier, RedirectUrl, CsrfToken}; +use serde::{Deserialize, Serialize}; + +use crate::Error; + +#[derive(Serialize, Deserialize)] +pub struct Macaroon { + pub idp_id: String, + pub nonce: Nonce, + pub csrf: CsrfToken, + pub redirect_url: Option, + pub pkce_verifier: Option, + pub time: i64, +} + +impl Macaroon { + pub fn encode(&self, macaroon: &str) -> Result { + jsonwebtoken::encode( + &Header::default(), + self, + &EncodingKey::from_secret(macaroon.as_bytes()), + ) + } + + pub fn verify(token: &str, macaroon: &str) -> Result { + let decoded = jsonwebtoken::decode::( + token, + &DecodingKey::from_secret(macaroon.as_bytes()), + &Validation::new(Algorithm::HS256), + ) + .map_err(|_| { + Error::BadRequest( + ruma::api::client::error::ErrorKind::Unauthorized, + "macaroon decoding", + ) + })?; + + Err(Error::BadRequest( + ruma::api::client::error::ErrorKind::Unauthorized, + "macaroon invalid", + )) + } +} diff --git a/src/service/sso/mod.rs b/src/service/sso/mod.rs index dfc7be1a..65aee6a1 100644 --- a/src/service/sso/mod.rs +++ b/src/service/sso/mod.rs @@ -1,24 +1,32 @@ use std::sync::Arc; +mod session; + use futures_util::future::{self}; -use macaroon::{Macaroon, Verifier}; use openidconnect::{ - core::{CoreAuthenticationFlow, CoreClient, CoreGenderClaim, CoreProviderMetadata}, + core::{ + CoreAuthenticationFlow, CoreClient, CoreGenderClaim, CoreIdTokenClaims, + CoreProviderMetadata, CoreUserInfoClaims, + }, reqwest::async_http_client, AccessTokenHash, AdditionalClaims, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, NonceVerifier, OAuth2TokenResponse, PkceCodeChallenge, - RedirectUrl, Scope, SubjectIdentifier, TokenResponse, TokenUrl, UserInfoClaims, UserInfoUrl, + PkceCodeVerifier, RedirectUrl, Scope, SubjectIdentifier, TokenResponse, TokenUrl, + UserInfoClaims, UserInfoUrl, }; use ruma::api::client::{error::ErrorKind, session::get_login_types::v3::IdentityProvider}; -use time::macros::format_description; +use time::{macros::format_description, OffsetDateTime}; use crate::{ config::{ClientConfig, DiscoveryConfig as Discovery, ProviderConfig}, services, Config, Error, }; +use self::macaroon::Macaroon; + pub const COOKIE_STATE_EXPIRATION_SECS: i64 = 60 * 60; +pub mod macaroon; pub mod templates; pub struct Service { @@ -128,116 +136,54 @@ impl Provider { Ok(Arc::new(config)) } - pub async fn handle_redirect(&self, redirect_url: &str) -> (url::Url, String, String) { + pub async fn handle_redirect(&self, redirect_url: &RedirectUrl) -> (url::Url, String, String) { let client = self.client.clone(); let scopes = self.scopes.iter().map(ToOwned::to_owned).map(Scope::new); let mut req = client .authorize_url( CoreAuthenticationFlow::Implicit(true), - || CsrfToken::new_random_len(36), - || Nonce::new_random_len(36), + || CsrfToken::new_random_len(48), + || Nonce::new_random_len(48), ) .add_scopes(scopes); - let (challenge, verifier) = PkceCodeChallenge::new_random_sha256(); - if let Some(true) = self.pkce { - req = req.set_pkce_challenge(challenge); - } + let pkce_verifier = match self.pkce { + Some(true) => { + let (challenge, verifier) = PkceCodeChallenge::new_random_sha256(); + req = req.set_pkce_challenge(challenge); + + Some(verifier) + } + _ => None, + }; let (url, csrf, nonce) = req.url(); - let cookie = self.generate_macaroon( - self.inner.id.as_str(), - csrf.secret(), - nonce.secret(), - redirect_url, - self.pkce.map(|_| verifier.secret().as_str()), - ); + let key = services() + .globals + .macaroon_key + .as_deref() + .expect("macaroon key") + .to_owned(); + let cookie = Macaroon { + idp_id: self.inner.id.clone(), + csrf, + nonce: nonce.clone(), + time: OffsetDateTime::now_utc().unix_timestamp(), + redirect_url: Some(redirect_url.clone()), + pkce_verifier, + }; + let cookie = cookie.encode(&key).expect("bad key"); (url, nonce.secret().to_owned(), cookie) } - pub fn generate_macaroon( - &self, - idp_id: &str, - state: &str, - nonce: &str, - redirect_url: &str, - pkce: Option<&str>, - ) -> String { - let key = services().globals.macaroon.unwrap(); - - let mut macaroon = Macaroon::create(None, &key, idp_id.into()).unwrap(); - let expires = (time::OffsetDateTime::now_utc() - + time::Duration::seconds(COOKIE_STATE_EXPIRATION_SECS)) - .to_string(); - - let idp_id = self.inner.id.as_str(); - - for caveat in [ - format!("idp_id = {idp_id}"), - format!("state = {state}"), - format!("nonce = {nonce}"), - format!("redirect_url = {redirect_url}"), - format!("time < {expires}"), - ] { - macaroon.add_first_party_caveat(caveat.into()); - } - - if let Some(verifier) = pkce { - macaroon.add_first_party_caveat(format!("verifier = {}", verifier).into()); - } - - macaroon.serialize(macaroon::Format::V2).unwrap() - } - - pub fn verify_macaroon(cookie: &[u8], state: CsrfToken) -> Result { - let mut verifier = Verifier::default(); - - let macaroon = Macaroon::deserialize(cookie).map_err(|e| { - Error::BadRequest(ErrorKind::BadJson, "Could not deserialize SSO macaroon") - })?; - - verifier.satisfy_exact(format!("state = {}", state.secret()).into()); - - // let verification = |s: &ByteString, id: &str| { - // s.0.starts_with(format!("{id} =").as_bytes()); // TODO - // }; - - verifier.satisfy_general(|s| s.0.starts_with(b"idp_id =")); - verifier.satisfy_general(|s| s.0.starts_with(b"nonce =")); - verifier.satisfy_general(|s| s.0.starts_with(b"redirect_url =")); - - verifier.satisfy_general(|s| { - let format_desc = format_description!( - "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \ - sign:mandatory]:[offset_minute]:[offset_second]" - ); - - let now = time::OffsetDateTime::now_utc(); - - time::OffsetDateTime::parse(std::str::from_utf8(&s.0).unwrap(), format_desc) - .map(|expires| now < expires) - .unwrap_or(false) - }); - - let key = services().globals.macaroon.unwrap(); - - verifier - .verify(&macaroon, &key, Default::default()) - .map_err(|e| { - Error::BadRequest(ErrorKind::Unauthorized, "Macaroon verification failed") - })?; - - Ok(macaroon) - } - pub async fn handle_callback( &self, code: AuthorizationCode, nonce: Nonce, - ) -> Result, Error> { + ) -> Result<(), Error> { let resp = self .client .exchange_code(code) @@ -260,14 +206,12 @@ impl Provider { } } - let Ok(req) = self.client.user_info( - resp.access_token().to_owned(), - self.subject_claim.clone().map(SubjectIdentifier::new), - ) else { - resp.extra_fields(); - panic!() - }; - - Ok(req.request_async(async_http_client).await.unwrap()) + // match self.client.user_info( + // resp.access_token().to_owned(), + // self.subject_claim.clone().map(SubjectIdentifier::new), + // ).map(|req| req.request_async(async_http_client)) { + // Err(e) => Ok(claims), + // Ok(req) => req.await, + // } } }