diff --git a/Cargo.lock b/Cargo.lock index 0dd79a40b..987fcec7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2777,11 +2777,11 @@ dependencies = [ "kitsune-core", "kitsune-db", "kitsune-embed", - "kitsune-http-client", "kitsune-http-signatures", "kitsune-job-runner", "kitsune-language", "kitsune-observability", + "kitsune-oidc", "kitsune-search", "kitsune-storage", "kitsune-test", @@ -2790,7 +2790,6 @@ dependencies = [ "mimalloc", "mime", "mime_guess", - "openidconnect", "oxide-auth", "oxide-auth-async", "oxide-auth-axum", @@ -3123,6 +3122,28 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "kitsune-oidc" +version = "0.0.1-pre.4" +dependencies = [ + "async-trait", + "deadpool-redis", + "enum_dispatch", + "http", + "hyper", + "kitsune-config", + "kitsune-http-client", + "moka", + "once_cell", + "openidconnect", + "redis", + "serde", + "simd-json", + "speedy-uuid", + "thiserror", + "url", +] + [[package]] name = "kitsune-retry-policies" version = "0.0.1-pre.4" @@ -4493,9 +4514,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b559898e0b4931ed2d3b959ab0c2da4d99cc644c4b0b1a35b4d344027f474023" +checksum = "3bccab0e7fd7cc19f820a1c8c91720af652d0c88dc9664dd72aef2614f04af3b" [[package]] name = "post-process" @@ -5167,9 +5188,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scraper" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3693f9a0203d49a7ba8f38aa915316b3d535c1862d03dae7009cb71a3408b36a" +checksum = "585480e3719b311b78a573db1c9d9c4c1f8010c2dee4cc59c2efe58ea4dbc3e1" dependencies = [ "ahash 0.8.6", "cssparser", @@ -6907,18 +6928,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7d7c7970ca2215b8c1ccf4d4f354c4733201dfaaba72d44ae5b37472e4901" +checksum = "dd66a62464e3ffd4e37bd09950c2b9dd6c4f8767380fabba0d523f9a775bc85a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b27b1bb92570f989aac0ab7e9cbfbacdd65973f7ee920d9f0e71ebac878fd0b" +checksum = "255c4596d41e6916ced49cfafea18727b24d67878fa180ddfd69b9df34fd1726" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 2de3c0441..0db5b8061 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ members = [ "crates/kitsune-language", "crates/kitsune-messaging", "crates/kitsune-observability", + "crates/kitsune-oidc", "crates/kitsune-retry-policies", "crates/kitsune-search", "crates/kitsune-storage", diff --git a/crates/kitsune-cache/src/redis.rs b/crates/kitsune-cache/src/redis.rs index fa8926b3b..c3caa63f3 100644 --- a/crates/kitsune-cache/src/redis.rs +++ b/crates/kitsune-cache/src/redis.rs @@ -28,7 +28,6 @@ impl Redis where K: ?Sized, { - #[allow(clippy::missing_panics_doc)] // All invariants covered. Won't panic. pub fn new

(redis_conn: deadpool_redis::Pool, prefix: P, ttl: Duration) -> Self where P: Into, diff --git a/crates/kitsune-config/src/lib.rs b/crates/kitsune-config/src/lib.rs index addf9af1f..b9ebb3939 100644 --- a/crates/kitsune-config/src/lib.rs +++ b/crates/kitsune-config/src/lib.rs @@ -39,7 +39,7 @@ pub struct MCaptchaConfiguration { } #[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", tag = "type")] +#[serde(rename_all = "lowercase", tag = "type")] pub enum CaptchaConfiguration { HCaptcha(HCaptchaConfiguration), MCaptcha(MCaptchaConfiguration), @@ -93,9 +93,23 @@ pub struct JobQueueConfiguration { pub num_workers: NonZeroUsize, } +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct OidcRedisStoreConfiguration { + pub url: SmolStr, +} + +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case", tag = "type")] +pub enum OidcStoreConfiguration { + InMemory, + Redis(OidcRedisStoreConfiguration), +} + #[derive(Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct OidcConfiguration { + pub store: OidcStoreConfiguration, pub server_url: SmolStr, pub client_id: SmolStr, pub client_secret: SmolStr, diff --git a/crates/kitsune-core/Cargo.toml b/crates/kitsune-core/Cargo.toml index 965fd6894..185a349c0 100644 --- a/crates/kitsune-core/Cargo.toml +++ b/crates/kitsune-core/Cargo.toml @@ -17,7 +17,7 @@ autometrics = { version = "0.6.0", default-features = false, features = [ base64-simd = "0.8.0" bytes = "1.5.0" const_format = "0.2.32" -deadpool-redis = { version = "0.13.0", default-features = false } +deadpool-redis = "0.13.0" derive_builder = "0.12.0" diesel = "2.1.3" diesel-async = { version = "0.4.1", features = ["postgres"] } diff --git a/crates/kitsune-embed/Cargo.toml b/crates/kitsune-embed/Cargo.toml index 2ff279788..6c8758ff9 100644 --- a/crates/kitsune-embed/Cargo.toml +++ b/crates/kitsune-embed/Cargo.toml @@ -12,7 +12,7 @@ iso8601-timestamp = "0.2.12" kitsune-db = { path = "../kitsune-db" } kitsune-http-client = { path = "../kitsune-http-client" } once_cell = "1.18.0" -scraper = { version = "0.18.0", default-features = false } +scraper = { version = "0.18.1", default-features = false } smol_str = "0.2.0" thiserror = "1.0.50" typed-builder = "0.18.0" diff --git a/crates/kitsune-oidc/Cargo.toml b/crates/kitsune-oidc/Cargo.toml new file mode 100644 index 000000000..a7b55b86e --- /dev/null +++ b/crates/kitsune-oidc/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "kitsune-oidc" +edition.workspace = true +version.workspace = true + +[dependencies] +async-trait = "0.1.74" +deadpool-redis = "0.13.0" +enum_dispatch = "0.3.12" +http = "0.2.9" +hyper = "0.14.27" +kitsune-config = { path = "../kitsune-config" } +kitsune-http-client = { path = "../kitsune-http-client" } +moka = { version = "0.12.1", features = ["sync"] } +once_cell = "1.18.0" +openidconnect = { version = "3.4.0", default-features = false, features = [ + # Accept these two, per specification invalid, cases to increase compatibility + "accept-rfc3339-timestamps", + "accept-string-booleans", +] } +redis = "0.23.3" +serde = { version = "1.0.190", features = ["derive"] } +simd-json = "0.13.4" +speedy-uuid = { path = "../../lib/speedy-uuid", features = ["serde"] } +thiserror = "1.0.50" +url = "2.4.1" diff --git a/crates/kitsune-oidc/src/error.rs b/crates/kitsune-oidc/src/error.rs new file mode 100644 index 000000000..318670161 --- /dev/null +++ b/crates/kitsune-oidc/src/error.rs @@ -0,0 +1,61 @@ +use openidconnect::{ + core::CoreErrorResponseType, ClaimsVerificationError, DiscoveryError, RequestTokenError, + SigningError, StandardErrorResponse, +}; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + ClaimsVerification(#[from] ClaimsVerificationError), + + #[error(transparent)] + CreateRedisPool(#[from] deadpool_redis::CreatePoolError), + + #[error(transparent)] + Discovery(#[from] DiscoveryError), + + #[error(transparent)] + JsonParse(#[from] simd_json::Error), + + #[error("Missing Email address")] + MissingEmail, + + #[error("Mismatching hash")] + MismatchingHash, + + #[error("Missing ID token")] + MissingIdToken, + + #[error("Missing login state")] + MissingLoginState, + + #[error("Missing username")] + MissingUsername, + + #[error(transparent)] + Redis(#[from] redis::RedisError), + + #[error(transparent)] + RedisPool(#[from] deadpool_redis::PoolError), + + #[error(transparent)] + RequestToken( + #[from] + RequestTokenError< + kitsune_http_client::Error, + StandardErrorResponse, + >, + ), + + #[error(transparent)] + Signing(#[from] SigningError), + + #[error("Unknown CSRF token")] + UnknownCsrfToken, + + #[error(transparent)] + UrlParse(#[from] url::ParseError), +} diff --git a/crates/kitsune-oidc/src/http.rs b/crates/kitsune-oidc/src/http.rs new file mode 100644 index 000000000..c38a50a08 --- /dev/null +++ b/crates/kitsune-oidc/src/http.rs @@ -0,0 +1,20 @@ +use http::Request; +use hyper::Body; +use kitsune_http_client::Client as HttpClient; +use once_cell::sync::Lazy; +use openidconnect::{HttpRequest, HttpResponse}; + +static HTTP_CLIENT: Lazy = Lazy::new(HttpClient::default); + +pub async fn async_client(req: HttpRequest) -> Result { + let mut request = Request::builder().method(req.method).uri(req.url.as_str()); + *request.headers_mut().unwrap() = req.headers; + let request = request.body(Body::from(req.body)).unwrap(); + let response = HTTP_CLIENT.execute(request).await?; + + Ok(HttpResponse { + status_code: response.status(), + headers: response.headers().clone(), + body: response.bytes().await?.to_vec(), + }) +} diff --git a/kitsune/src/oidc.rs b/crates/kitsune-oidc/src/lib.rs similarity index 50% rename from kitsune/src/oidc.rs rename to crates/kitsune-oidc/src/lib.rs index c53f0b4e0..7bcc5e44b 100644 --- a/kitsune/src/oidc.rs +++ b/crates/kitsune-oidc/src/lib.rs @@ -1,31 +1,36 @@ -use crate::error::{OidcError, Result}; -use http::Request; -use hyper::Body; -use kitsune_cache::{ArcCache, CacheBackend}; -use kitsune_http_client::{Client, Error}; +#![forbid(rust_2018_idioms, unsafe_code)] +#![warn(clippy::all, clippy::pedantic)] +#![allow( + clippy::missing_errors_doc, + clippy::missing_panics_doc, + clippy::module_name_repetitions, + forbidden_lint_groups +)] + +use crate::{ + error::Result, + state::{ + store::{InMemory as InMemoryStore, Redis as RedisStore}, + LoginState, OAuth2LoginState, Store, + }, +}; +use kitsune_config::{OidcConfiguration, OidcStoreConfiguration}; use openidconnect::{ - core::{CoreAuthenticationFlow, CoreClient}, - AccessTokenHash, AuthorizationCode, CsrfToken, HttpRequest, HttpResponse, Nonce, - OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, Scope, TokenResponse, + core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata}, + AccessTokenHash, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, + OAuth2TokenResponse, PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, }; -use serde::{Deserialize, Serialize}; use speedy_uuid::Uuid; -use typed_builder::TypedBuilder; use url::Url; -#[allow(clippy::missing_panics_doc)] -pub async fn async_client(req: HttpRequest) -> Result { - let mut request = Request::builder().method(req.method).uri(req.url.as_str()); - *request.headers_mut().unwrap() = req.headers; - let request = request.body(Body::from(req.body)).unwrap(); - let response = Client::default().execute(request).await?; - - Ok(HttpResponse { - status_code: response.status(), - headers: response.headers().clone(), - body: response.bytes().await?.to_vec(), - }) -} +pub use self::error::Error; + +mod error; +mod state; + +pub mod http; + +const LOGIN_STATE_STORE_SIZE: u64 = 100; #[derive(Debug)] pub struct OAuth2Info { @@ -42,37 +47,44 @@ pub struct UserInfo { pub oauth2: OAuth2Info, } -#[derive(Clone, Deserialize, Serialize)] -pub struct OAuth2LoginState { - application_id: Uuid, - scope: String, - state: Option, -} - -#[derive(Deserialize, Serialize)] -pub struct LoginState { - nonce: Nonce, - pkce_verifier: PkceCodeVerifier, - oauth2: OAuth2LoginState, -} - -impl Clone for LoginState { - fn clone(&self) -> Self { - Self { - nonce: self.nonce.clone(), - pkce_verifier: PkceCodeVerifier::new(self.pkce_verifier.secret().clone()), - oauth2: self.oauth2.clone(), - } - } -} - -#[derive(Clone, TypedBuilder)] +#[derive(Clone)] pub struct OidcService { client: CoreClient, - login_state: ArcCache, + login_state_store: self::state::StoreBackend, } impl OidcService { + #[inline] + pub async fn initialise(config: &OidcConfiguration, redirect_uri: String) -> Result { + let provider_metadata = CoreProviderMetadata::discover_async( + IssuerUrl::new(config.server_url.to_string())?, + self::http::async_client, + ) + .await?; + + let client = CoreClient::from_provider_metadata( + provider_metadata, + ClientId::new(config.client_id.to_string()), + Some(ClientSecret::new(config.client_secret.to_string())), + ) + .set_redirect_uri(RedirectUrl::new(redirect_uri)?); + + let login_state_store = match config.store { + OidcStoreConfiguration::InMemory => InMemoryStore::new(LOGIN_STATE_STORE_SIZE).into(), + OidcStoreConfiguration::Redis(ref redis_config) => { + let config = deadpool_redis::Config::from_url(redis_config.url.clone()); + let pool = config.create_pool(Some(deadpool_redis::Runtime::Tokio1))?; + + RedisStore::new(pool).into() + } + }; + + Ok(Self { + client, + login_state_store, + }) + } + pub async fn authorisation_url( &self, oauth2_application_id: Uuid, @@ -101,8 +113,8 @@ impl OidcService { state: oauth2_state, }, }; - self.login_state - .set(csrf_token.secret(), &verification_data) + self.login_state_store + .set(csrf_token.secret(), verification_data) .await?; Ok(auth_url) @@ -112,26 +124,21 @@ impl OidcService { &self, state: String, authorization_code: String, - ) -> Result { + ) -> Result { let LoginState { nonce, oauth2, pkce_verifier, - } = self - .login_state - .get(&state) - .await? - .ok_or(OidcError::UnknownCsrfToken)?; - self.login_state.delete(&state).await?; + } = self.login_state_store.get_and_remove(&state).await?; let token_response = self .client .exchange_code(AuthorizationCode::new(authorization_code)) .set_pkce_verifier(pkce_verifier) - .request_async(async_client) + .request_async(self::http::async_client) .await?; - let id_token = token_response.id_token().ok_or(OidcError::MissingIdToken)?; + let id_token = token_response.id_token().ok_or(Error::MissingIdToken)?; let claims = id_token.claims(&self.client.id_token_verifier(), &nonce)?; if let Some(expected_hash) = claims.access_token_hash() { @@ -141,7 +148,7 @@ impl OidcService { )?; if actual_hash != *expected_hash { - return Err(OidcError::MismatchingHash); + return Err(Error::MismatchingHash); } } @@ -149,9 +156,9 @@ impl OidcService { subject: claims.subject().to_string(), username: claims .preferred_username() - .ok_or(OidcError::MissingUsername)? + .ok_or(Error::MissingUsername)? .to_string(), - email: claims.email().ok_or(OidcError::MissingEmail)?.to_string(), + email: claims.email().ok_or(Error::MissingEmail)?.to_string(), oauth2: OAuth2Info { application_id: oauth2.application_id, scope: oauth2.scope, diff --git a/crates/kitsune-oidc/src/state/mod.rs b/crates/kitsune-oidc/src/state/mod.rs new file mode 100644 index 000000000..f401a2d77 --- /dev/null +++ b/crates/kitsune-oidc/src/state/mod.rs @@ -0,0 +1,31 @@ +use openidconnect::{Nonce, PkceCodeVerifier}; +use serde::{Deserialize, Serialize}; +use speedy_uuid::Uuid; + +pub use self::store::{Store, StoreBackend}; + +pub mod store; + +#[derive(Clone, Deserialize, Serialize)] +pub struct OAuth2LoginState { + pub application_id: Uuid, + pub scope: String, + pub state: Option, +} + +#[derive(Deserialize, Serialize)] +pub struct LoginState { + pub nonce: Nonce, + pub pkce_verifier: PkceCodeVerifier, + pub oauth2: OAuth2LoginState, +} + +impl Clone for LoginState { + fn clone(&self) -> Self { + Self { + nonce: self.nonce.clone(), + pkce_verifier: PkceCodeVerifier::new(self.pkce_verifier.secret().clone()), + oauth2: self.oauth2.clone(), + } + } +} diff --git a/crates/kitsune-oidc/src/state/store/in_memory.rs b/crates/kitsune-oidc/src/state/store/in_memory.rs new file mode 100644 index 000000000..6c4741366 --- /dev/null +++ b/crates/kitsune-oidc/src/state/store/in_memory.rs @@ -0,0 +1,32 @@ +use super::Store; +use crate::{ + error::{Error, Result}, + state::LoginState, +}; +use async_trait::async_trait; +use moka::sync::Cache; + +#[derive(Clone)] +pub struct InMemory { + inner: Cache, +} + +impl InMemory { + pub fn new(size: u64) -> Self { + Self { + inner: Cache::new(size), + } + } +} + +#[async_trait] +impl Store for InMemory { + async fn get_and_remove(&self, key: &str) -> Result { + self.inner.remove(key).ok_or(Error::MissingLoginState) + } + + async fn set(&self, key: &str, value: LoginState) -> Result<()> { + self.inner.insert(key.to_string(), value); + Ok(()) + } +} diff --git a/crates/kitsune-oidc/src/state/store/mod.rs b/crates/kitsune-oidc/src/state/store/mod.rs new file mode 100644 index 000000000..6c115ea7f --- /dev/null +++ b/crates/kitsune-oidc/src/state/store/mod.rs @@ -0,0 +1,23 @@ +use super::LoginState; +use crate::error::Result; +use async_trait::async_trait; +use enum_dispatch::enum_dispatch; + +pub use self::{in_memory::InMemory, redis::Redis}; + +pub mod in_memory; +pub mod redis; + +#[derive(Clone)] +#[enum_dispatch(Store)] +pub enum StoreBackend { + InMemory(in_memory::InMemory), + Redis(redis::Redis), +} + +#[async_trait] +#[enum_dispatch] +pub trait Store { + async fn get_and_remove(&self, key: &str) -> Result; + async fn set(&self, key: &str, value: LoginState) -> Result<()>; +} diff --git a/crates/kitsune-oidc/src/state/store/redis.rs b/crates/kitsune-oidc/src/state/store/redis.rs new file mode 100644 index 000000000..2352e028a --- /dev/null +++ b/crates/kitsune-oidc/src/state/store/redis.rs @@ -0,0 +1,41 @@ +use super::Store; +use crate::{error::Result, state::LoginState}; +use async_trait::async_trait; +use redis::AsyncCommands; + +const REDIS_PREFIX: &str = "OIDC-LOGIN-STATE"; + +#[derive(Clone)] +pub struct Redis { + pool: deadpool_redis::Pool, +} + +impl Redis { + pub fn new(pool: deadpool_redis::Pool) -> Self { + Self { pool } + } + + #[inline] + fn format_key(key: &str) -> String { + format!("{REDIS_PREFIX}:{key}") + } +} + +#[async_trait] +impl Store for Redis { + async fn get_and_remove(&self, key: &str) -> Result { + let mut conn = self.pool.get().await?; + let raw_value: String = conn.get_del(Self::format_key(key)).await?; + + let mut raw_value = raw_value.into_bytes(); + Ok(simd_json::from_slice(&mut raw_value)?) + } + + async fn set(&self, key: &str, value: LoginState) -> Result<()> { + let raw_value = simd_json::to_string(&value)?; + let mut conn = self.pool.get().await?; + conn.set(Self::format_key(key), raw_value).await?; + + Ok(()) + } +} diff --git a/kitsune/Cargo.toml b/kitsune/Cargo.toml index d3ebc1f70..967e0bda9 100644 --- a/kitsune/Cargo.toml +++ b/kitsune/Cargo.toml @@ -43,7 +43,6 @@ kitsune-config = { path = "../crates/kitsune-config" } kitsune-core = { path = "../crates/kitsune-core" } kitsune-db = { path = "../crates/kitsune-db" } kitsune-embed = { path = "../crates/kitsune-embed" } -kitsune-http-client = { path = "../crates/kitsune-http-client" } kitsune-http-signatures = { path = "../crates/kitsune-http-signatures" } kitsune-job-runner = { path = "../kitsune-job-runner" } kitsune-language = { path = "../crates/kitsune-language" } @@ -97,7 +96,7 @@ async-graphql = { version = "6.0.9", default-features = false, features = [ async-graphql-axum = { version = "6.0.9", optional = true } # "oidc" feature -openidconnect = { version = "3.4.0", default-features = false, optional = true } +kitsune-oidc = { path = "../crates/kitsune-oidc", optional = true } [dev-dependencies] kitsune-test = { path = "../crates/kitsune-test" } @@ -114,4 +113,4 @@ graphql-api = [ ] mastodon-api = ["kitsune-core/mastodon-api"] meilisearch = ["kitsune-core/meilisearch"] -oidc = ["dep:openidconnect"] +oidc = ["dep:kitsune-oidc"] diff --git a/kitsune/src/error.rs b/kitsune/src/error.rs index 7450ff7c9..48a420e19 100644 --- a/kitsune/src/error.rs +++ b/kitsune/src/error.rs @@ -43,7 +43,7 @@ pub enum Error { #[cfg(feature = "oidc")] #[error(transparent)] - Oidc(#[from] OidcError), + Oidc(#[from] kitsune_oidc::Error), #[error(transparent)] ParseBool(#[from] ParseBoolError), @@ -88,49 +88,6 @@ pub enum OAuth2Error { Web(#[from] oxide_auth_axum::WebError), } -#[cfg(feature = "oidc")] -use openidconnect::{ - core::CoreErrorResponseType, ClaimsVerificationError, RequestTokenError, SigningError, - StandardErrorResponse, -}; - -#[cfg(feature = "oidc")] -#[derive(Debug, Error)] -pub enum OidcError { - #[error(transparent)] - ClaimsVerification(#[from] ClaimsVerificationError), - - #[error(transparent)] - LoginState(#[from] kitsune_cache::Error), - - #[error("Missing Email address")] - MissingEmail, - - #[error("Mismatching hash")] - MismatchingHash, - - #[error("Missing ID token")] - MissingIdToken, - - #[error("Missing username")] - MissingUsername, - - #[error(transparent)] - RequestToken( - #[from] - RequestTokenError< - kitsune_http_client::Error, - StandardErrorResponse, - >, - ), - - #[error(transparent)] - Signing(#[from] SigningError), - - #[error("Unknown CSRF token")] - UnknownCsrfToken, -} - impl From for Error { fn from(value: ApiError) -> Self { Self::Core(value.into()) diff --git a/kitsune/src/http/handler/oauth/authorize.rs b/kitsune/src/http/handler/oauth/authorize.rs index 429906396..567394027 100644 --- a/kitsune/src/http/handler/oauth/authorize.rs +++ b/kitsune/src/http/handler/oauth/authorize.rs @@ -27,9 +27,9 @@ use speedy_uuid::Uuid; #[cfg(feature = "oidc")] use { - crate::oidc::OidcService, axum::extract::Query, kitsune_db::{model::oauth2, schema::oauth2_applications}, + kitsune_oidc::OidcService, }; #[cfg(feature = "oidc")] diff --git a/kitsune/src/http/handler/oidc/callback.rs b/kitsune/src/http/handler/oidc/callback.rs index b3faa8045..ee47c459c 100644 --- a/kitsune/src/http/handler/oidc/callback.rs +++ b/kitsune/src/http/handler/oidc/callback.rs @@ -1,7 +1,6 @@ use crate::{ error::{OAuth2Error, Result}, oauth2::{AuthorisationCode, OAuth2Service}, - oidc::OidcService, }; use axum::{ extract::{Query, State}, @@ -17,6 +16,7 @@ use kitsune_db::{ schema::{oauth2_applications, users}, PgPool, }; +use kitsune_oidc::OidcService; use scoped_futures::ScopedFutureExt; use serde::Deserialize; diff --git a/kitsune/src/lib.rs b/kitsune/src/lib.rs index 7a72264ca..c6eebb3b6 100644 --- a/kitsune/src/lib.rs +++ b/kitsune/src/lib.rs @@ -17,55 +17,19 @@ pub mod consts; pub mod error; pub mod http; pub mod oauth2; -#[cfg(feature = "oidc")] -pub mod oidc; pub mod state; use self::{ - oauth2::OAuth2Service, + oauth2::{OAuth2Service, OAuthEndpoint}, state::{SessionConfig, Zustand}, }; use athena::JobQueue; use kitsune_config::Configuration; use kitsune_core::job::KitsuneContextRepo; use kitsune_db::PgPool; -use oauth2::OAuthEndpoint; #[cfg(feature = "oidc")] -use { - self::oidc::{async_client, OidcService}, - futures_util::future::OptionFuture, - kitsune_config::OidcConfiguration, - kitsune_core::service::url::UrlService, - openidconnect::{ - core::{CoreClient, CoreProviderMetadata}, - ClientId, ClientSecret, IssuerUrl, RedirectUrl, - }, -}; - -#[cfg(feature = "oidc")] -async fn prepare_oidc_client( - oidc_config: &OidcConfiguration, - url_service: &UrlService, -) -> eyre::Result { - use eyre::Context; - - let provider_metadata = CoreProviderMetadata::discover_async( - IssuerUrl::new(oidc_config.server_url.to_string()).context("Invalid OIDC issuer URL")?, - async_client, - ) - .await - .context("Couldn't discover the OIDC provider metadata")?; - - let client = CoreClient::from_provider_metadata( - provider_metadata, - ClientId::new(oidc_config.client_id.to_string()), - Some(ClientSecret::new(oidc_config.client_secret.to_string())), - ) - .set_redirect_uri(RedirectUrl::new(url_service.oidc_redirect_uri())?); - - Ok(client) -} +use {futures_util::future::OptionFuture, kitsune_oidc::OidcService}; pub async fn initialise_state( config: &Configuration, @@ -75,13 +39,8 @@ pub async fn initialise_state( let core_state = kitsune_core::prepare_state(config, conn.clone(), job_queue).await?; #[cfg(feature = "oidc")] - let oidc_service = OptionFuture::from(config.server.oidc.as_ref().map(|oidc_config| async { - let service = OidcService::builder() - .client(prepare_oidc_client(oidc_config, &core_state.service.url).await?) - .login_state(kitsune_core::prepare_cache(config, "OIDC-LOGIN-STATE")) // TODO: REPLACE THIS WITH A BETTER ALTERNATIVE TO JUST ABUSING A CACHE - .build(); - - Ok::<_, eyre::Report>(service) + let oidc_service = OptionFuture::from(config.server.oidc.as_ref().map(|oidc_config| { + OidcService::initialise(oidc_config, core_state.service.url.oidc_redirect_uri()) })) .await .transpose()?; diff --git a/kitsune/src/state.rs b/kitsune/src/state.rs index 786819f41..4877a256e 100644 --- a/kitsune/src/state.rs +++ b/kitsune/src/state.rs @@ -19,7 +19,7 @@ use kitsune_embed::Client as EmbedClient; use kitsune_core::mapping::MastodonMapper; #[cfg(feature = "oidc")] -use crate::oidc::OidcService; +use kitsune_oidc::OidcService; #[macro_export] macro_rules! impl_from_ref {