aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/api-auth/Cargo.toml1
-rw-r--r--crates/api-auth/src/discord/mod.rs26
-rw-r--r--crates/api-auth/src/error.rs6
-rw-r--r--crates/api-auth/src/lib.rs8
-rw-r--r--crates/api-core/src/auth/provider.rs2
-rw-r--r--crates/sellershut/src/server/api/routes/auth/discord.rs64
-rw-r--r--crates/sellershut/src/server/api/routes/auth/mod.rs64
-rw-r--r--crates/sh-util/src/cache/key.rs23
-rw-r--r--crates/sh-util/src/cache/mod.rs2
-rw-r--r--crates/sh-util/src/cache/sentinel.rs57
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(&params.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
- }
-}