finished for real now

This commit is contained in:
mikoto 2024-02-28 12:30:09 +00:00
parent be13266eda
commit 03d312ca68
6 changed files with 137 additions and 227 deletions

89
Cargo.lock generated
View file

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

View file

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

View file

@ -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,12 +50,14 @@ 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")
// .secure(false) //FIXME
@ -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::headers::Cookie>,
axum::extract::Query(callback): axum::extract::Query<Callback>,
) -> 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()
}

View file

@ -75,7 +75,7 @@ pub struct Service {
pub rotate: RotationHandler,
pub shutdown: AtomicBool,
pub macaroon: Option<macaroon::MacaroonKey>,
pub macaroon_key: Option<String>,
}
/// 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())?;

View file

@ -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<RedirectUrl>,
pub pkce_verifier: Option<PkceCodeVerifier>,
pub time: i64,
}
impl Macaroon {
pub fn encode(&self, macaroon: &str) -> Result<String, jsonwebtoken::errors::Error> {
jsonwebtoken::encode(
&Header::default(),
self,
&EncodingKey::from_secret(macaroon.as_bytes()),
)
}
pub fn verify(token: &str, macaroon: &str) -> Result<Self, Error> {
let decoded = jsonwebtoken::decode::<Self>(
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",
))
}
}

View file

@ -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 pkce_verifier = match self.pkce {
Some(true) => {
let (challenge, verifier) = PkceCodeChallenge::new_random_sha256();
if let Some(true) = self.pkce {
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<Macaroon, Error> {
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<Claims: AdditionalClaims>(
&self,
code: AuthorizationCode,
nonce: Nonce,
) -> Result<UserInfoClaims<Claims, CoreGenderClaim>, 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,
// }
}
}