diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/api-auth/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/api-auth/src/discord/mod.rs | 26 | ||||
| -rw-r--r-- | crates/api-auth/src/error.rs | 6 | ||||
| -rw-r--r-- | crates/api-auth/src/lib.rs | 8 | ||||
| -rw-r--r-- | crates/api-core/src/auth/provider.rs | 2 | ||||
| -rw-r--r-- | crates/sellershut/src/server/api/routes/auth/discord.rs | 64 | ||||
| -rw-r--r-- | crates/sellershut/src/server/api/routes/auth/mod.rs | 64 | ||||
| -rw-r--r-- | crates/sh-util/src/cache/key.rs | 23 | ||||
| -rw-r--r-- | crates/sh-util/src/cache/mod.rs | 2 | ||||
| -rw-r--r-- | crates/sh-util/src/cache/sentinel.rs | 57 |
10 files changed, 118 insertions, 135 deletions
diff --git a/crates/api-auth/Cargo.toml b/crates/api-auth/Cargo.toml index a0868a5..5ce0647 100644 --- a/crates/api-auth/Cargo.toml +++ b/crates/api-auth/Cargo.toml @@ -11,6 +11,7 @@ homepage.workspace = true api-core = { workspace = true, features = ["auth", "users"] } async-trait.workspace = true oauth2 = "5.0.0" +redis.workspace = true secrecy.workspace = true serde.workspace = true sh-util = { workspace = true, optional = true } diff --git a/crates/api-auth/src/discord/mod.rs b/crates/api-auth/src/discord/mod.rs index dbcb139..1a7d47d 100644 --- a/crates/api-auth/src/discord/mod.rs +++ b/crates/api-auth/src/discord/mod.rs @@ -1,11 +1,12 @@ use api_core::models::user::User; -use async_session::Session; +use async_session::{Session, serde_json}; use async_trait::async_trait; use oauth2::{CsrfToken, Scope}; -use sh_util::cache::RedisManager; +use redis::AsyncCommands; +use sh_util::cache::{CacheKey, RedisManager}; use sqlx::PgPool; -use crate::{BasicClient, CSRF_TOKEN, OauthDriver, error::AuthError}; +use crate::{BasicClient, CSRF_TOKEN, OauthDriver, SessionResponse, error::AuthError}; #[derive(Clone)] pub struct AuthServiceDiscord { @@ -32,7 +33,7 @@ impl OauthDriver for AuthServiceDiscord { async fn get_user(&self) -> Result<User, AuthError> { todo!() } - async fn create_oauth_session(&self) -> Result<String, AuthError> { + async fn create_oauth_session(&self) -> Result<SessionResponse, AuthError> { let (auth_url, csrf_token) = self .client .authorize_url(CsrfToken::new_random) @@ -42,7 +43,22 @@ impl OauthDriver for AuthServiceDiscord { let mut session = Session::new(); session.insert(CSRF_TOKEN, &csrf_token).unwrap(); - Ok(String::default()) + let cache_key = CacheKey::Session(session.id()); + let mut cache = self.cache.get().await.unwrap(); + cache + .set::<_, _, ()>( + cache_key, + serde_json::to_string(&session).or(Err(AuthError::InvalidSession))?, + ) + .await?; + let cookie = session + .into_cookie_value() + .ok_or(AuthError::MissingSession)?; + + Ok(SessionResponse { + cookie_value: cookie, + auth_url, + }) } async fn save_session(&self, user: &User) -> Result<(), AuthError> { todo!() diff --git a/crates/api-auth/src/error.rs b/crates/api-auth/src/error.rs index ec60e51..72a7fba 100644 --- a/crates/api-auth/src/error.rs +++ b/crates/api-auth/src/error.rs @@ -22,4 +22,10 @@ pub enum AuthError { InvalidTokenUrl(#[source] oauth2::url::ParseError), #[error("invalid redirect url: {0}")] InvalidRedirectUrl(#[source] oauth2::url::ParseError), + #[error("cache")] + Cache(#[from] redis::RedisError), + #[error("missing session")] + MissingSession, + #[error("invalid session")] + InvalidSession, } diff --git a/crates/api-auth/src/lib.rs b/crates/api-auth/src/lib.rs index 367d395..85fdb01 100644 --- a/crates/api-auth/src/lib.rs +++ b/crates/api-auth/src/lib.rs @@ -23,17 +23,23 @@ pub struct BasicClient(C); pub trait OauthDriver: Send + Sync { async fn get_auth_token(&self) -> Result<String, AuthError>; async fn get_user(&self) -> Result<User, AuthError>; - async fn create_oauth_session(&self) -> Result<String, AuthError>; + async fn create_oauth_session(&self) -> Result<SessionResponse, AuthError>; async fn save_session(&self, user: &User) -> Result<(), AuthError>; } use oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; use std::{convert::TryFrom, ops::Deref}; +use url::Url; use crate::error::AuthError; static CSRF_TOKEN: &str = "csrf_token"; +pub struct SessionResponse { + pub cookie_value: String, + pub auth_url: Url, +} + impl Deref for BasicClient { type Target = C; diff --git a/crates/api-core/src/auth/provider.rs b/crates/api-core/src/auth/provider.rs index 803472f..7bcd504 100644 --- a/crates/api-core/src/auth/provider.rs +++ b/crates/api-core/src/auth/provider.rs @@ -4,7 +4,7 @@ #[cfg_attr( feature = "utoipa", derive(utoipa::ToSchema, serde::Deserialize, serde::Serialize), - schema(example = "v0"), + schema(example = "discord"), serde(rename_all = "camelCase") )] pub enum OauthProvider { diff --git a/crates/sellershut/src/server/api/routes/auth/discord.rs b/crates/sellershut/src/server/api/routes/auth/discord.rs deleted file mode 100644 index 0296e48..0000000 --- a/crates/sellershut/src/server/api/routes/auth/discord.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::server::api::error::AppError; -use anyhow::Context; -use api_core::auth::provider::OauthProvider; -use axum::{ - extract::State, - http::HeaderMap, - response::{IntoResponse, Redirect}, -}; - -use crate::state::AppState; - -/// Update log level -#[utoipa::path( - patch, - responses( - ( - status = 200, - description = "A redirect to discord", - headers( - ("x-request-id", description = "Request identifier") - ) - ), - ), - operation_id = "auth_discord", // https://github.com/juhaku/utoipa/issues/1170 - path = "/auth", - tag = super::AUTH, -)] -pub async fn discord_auth(State(state): State<AppState>) -> Result<impl IntoResponse, AppError> { - let client = state - .auth_clients - .get(&OauthProvider::Discord) - .context("missing discord driver")?; - - let headers = HeaderMap::new(); - Ok((headers, Redirect::to("/"))) - - // let (auth_url, csrf_token) = client - // .authorize_url(CsrfToken::new_random) - // .add_scope(Scope::new("identify".to_string())) - // .url(); - // - // // Create session to store csrf_token - // let mut session = Session::new(); - // session - // .insert(CSRF_TOKEN, &csrf_token) - // .context("failed in inserting CSRF token into session")?; - // - // // Store the session in MemoryStore and retrieve the session cookie - // let cookie = store - // .store_session(session) - // .await - // .context("failed to store CSRF token session")? - // .context("unexpected error retrieving CSRF cookie value")?; - // - // // Attach the session cookie to the response header - // let cookie = format!("{COOKIE_NAME}={cookie}; SameSite=Lax; HttpOnly; Secure; Path=/"); - // let mut headers = HeaderMap::new(); - // headers.insert( - // SET_COOKIE, - // cookie.parse().context("failed to parse cookie")?, - // ); - // - // Ok((headers, Redirect::to(auth_url.as_ref()))) -} diff --git a/crates/sellershut/src/server/api/routes/auth/mod.rs b/crates/sellershut/src/server/api/routes/auth/mod.rs index 3e36eaa..9efd542 100644 --- a/crates/sellershut/src/server/api/routes/auth/mod.rs +++ b/crates/sellershut/src/server/api/routes/auth/mod.rs @@ -1,22 +1,72 @@ -#[cfg(feature = "auth-discord")] -mod discord; - -use utoipa::OpenApi; +use anyhow::Context; +use api_core::auth::provider::OauthProvider; +use axum::{extract::{Query, State}, http::{HeaderMap, header::SET_COOKIE}, response::{IntoResponse, Redirect}}; +use serde::Deserialize; +use utoipa::{IntoParams, OpenApi}; use utoipa_axum::router::OpenApiRouter; -use crate::state::AppState; +use crate::{server::api::error::AppError, state::AppState}; const AUTH: &str = "Authentication"; +static COOKIE_NAME: &str = "SESSION"; + #[derive(OpenApi)] -#[openapi(tags((name = AUTH, description = "Transaction monitoring endpoints")))] +#[openapi(tags((name = AUTH, description = "Transaction monitoring endpoints")),components(schemas(OauthProvider))) ] pub struct AuthDoc; +/// Oauth provider +#[derive(Deserialize, IntoParams)] +pub struct OauthParams { + provider: OauthProvider, +} + pub fn router(store: AppState) -> OpenApiRouter<AppState> { let router = OpenApiRouter::new(); #[cfg(feature = "auth-discord")] - let router = router.routes(utoipa_axum::routes!(discord::discord_auth)); + let router = router.routes(utoipa_axum::routes!(auth)); router.with_state(store) } + +/// Initiate oauth flow +#[utoipa::path( + get, + responses( + ( + status = 302, + description = "A redirect to the provider's auth URL", + headers( + ("x-request-id", description = "Request identifier"), + ("set-cookie", description = "Oauth session cookie") + ) + ), + ), + operation_id = "auth", // https://github.com/juhaku/utoipa/issues/1170 + path = "/auth", + tag = AUTH, + params(OauthParams) +)] +pub async fn auth( + Query(params): Query<OauthParams>, + State(state): State<AppState>) -> Result<impl IntoResponse, AppError> { + let client = state + .auth_clients + .get(¶ms.provider) + .context("missing discord driver")?; + + let session = client.create_oauth_session().await?; + // Attach the session cookie to the response header + let cookie = format!( + "{COOKIE_NAME}={}; SameSite=Lax; HttpOnly; Secure; Path=/", + session.cookie_value + ); + let mut headers = HeaderMap::new(); + headers.insert( + SET_COOKIE, + cookie.parse().context("failed to parse cookie")?, + ); + + Ok((headers, Redirect::to(session.auth_url.as_ref()))) +} diff --git a/crates/sh-util/src/cache/key.rs b/crates/sh-util/src/cache/key.rs new file mode 100644 index 0000000..41315b8 --- /dev/null +++ b/crates/sh-util/src/cache/key.rs @@ -0,0 +1,23 @@ +use redis::{ToRedisArgs, ToSingleRedisArg}; + +pub enum CacheKey<'a> { + Session(&'a str), +} + +impl ToRedisArgs for CacheKey<'_> { + fn write_redis_args<W>(&self, out: &mut W) + where + W: ?Sized + redis::RedisWrite, + { + out.write_arg( + match self { + CacheKey::Session(id) => { + format!("session:{id}") + } + } + .as_bytes(), + ); + } +} + +impl ToSingleRedisArg for CacheKey<'_> {} diff --git a/crates/sh-util/src/cache/mod.rs b/crates/sh-util/src/cache/mod.rs index 67a5121..575e776 100644 --- a/crates/sh-util/src/cache/mod.rs +++ b/crates/sh-util/src/cache/mod.rs @@ -1,3 +1,5 @@ +mod key; +pub use key::*; mod cluster; mod sentinel; pub use sentinel::SentinelConfig; diff --git a/crates/sh-util/src/cache/sentinel.rs b/crates/sh-util/src/cache/sentinel.rs index e52b043..5406c4d 100644 --- a/crates/sh-util/src/cache/sentinel.rs +++ b/crates/sh-util/src/cache/sentinel.rs @@ -1,12 +1,3 @@ -use futures_util::lock::Mutex; -use redis::{ - ErrorKind, IntoConnectionInfo, RedisError, - sentinel::{SentinelClient, SentinelNodeConnectionInfo, SentinelServerType}, -}; -use serde::Deserialize; - -struct LockedSentinelClient(pub(crate) Mutex<SentinelClient>); - #[derive(Clone, Debug, Eq, PartialEq)] pub struct SentinelConfig { pub service_name: String, @@ -16,51 +7,3 @@ pub struct SentinelConfig { pub redis_password: String, pub redis_use_resp3: bool, } - -/// ConnectionManager that implements `bb8::ManageConnection` and supports -/// asynchronous Sentinel connections via `redis::sentinel::SentinelClient` -pub struct RedisSentinelConnectionManager { - client: LockedSentinelClient, -} - -impl RedisSentinelConnectionManager { - pub fn new<T: IntoConnectionInfo>( - info: Vec<T>, - service_name: String, - node_connection_info: Option<SentinelNodeConnectionInfo>, - ) -> Result<RedisSentinelConnectionManager, RedisError> { - Ok(RedisSentinelConnectionManager { - client: LockedSentinelClient(Mutex::new(SentinelClient::build( - info, - service_name, - node_connection_info, - SentinelServerType::Master, - )?)), - }) - } -} - -impl bb8::ManageConnection for RedisSentinelConnectionManager { - type Connection = redis::aio::MultiplexedConnection; - type Error = RedisError; - - async fn connect(&self) -> Result<Self::Connection, Self::Error> { - self.client.0.lock().await.get_async_connection().await - } - - async fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> { - let pong: String = redis::cmd("PING").query_async(conn).await?; - match pong.as_str() { - "PONG" => Ok(()), - _ => Err(( - ErrorKind::Server(redis::ServerErrorKind::ResponseError), - "ping request", - ) - .into()), - } - } - - fn has_broken(&self, _: &mut Self::Connection) -> bool { - false - } -} |
