diff options
-rw-r--r-- | Cargo.lock | 2 | ||||
-rw-r--r-- | crates/auth/Cargo.toml | 1 | ||||
-rw-r--r-- | crates/auth/auth.toml | 1 | ||||
-rw-r--r-- | crates/auth/migrations/20250723100947_user.sql | 19 | ||||
-rw-r--r-- | crates/auth/migrations/20250725160900_session.sql | 8 | ||||
-rw-r--r-- | crates/auth/migrations/20250725161014_token.sql | 9 | ||||
-rw-r--r-- | crates/auth/src/cnfg.rs | 1 | ||||
-rw-r--r-- | crates/auth/src/server/routes.rs | 15 | ||||
-rw-r--r-- | crates/auth/src/server/routes/authorised.rs | 137 |
9 files changed, 183 insertions, 10 deletions
@@ -213,6 +213,7 @@ dependencies = [ "tower-sessions-sqlx-store", "tracing", "url", + "uuid", ] [[package]] @@ -3278,6 +3279,7 @@ checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ "getrandom 0.3.3", "js-sys", + "serde", "wasm-bindgen", ] diff --git a/crates/auth/Cargo.toml b/crates/auth/Cargo.toml index ccd18f3..410c51e 100644 --- a/crates/auth/Cargo.toml +++ b/crates/auth/Cargo.toml @@ -32,6 +32,7 @@ tower-sessions-moka-store = "0.15.0" tower-sessions-sqlx-store = { version = "0.15.0", features = ["postgres"] } tracing.workspace = true url.workspace = true +uuid = { workspace = true, features = ["serde", "v7"] } [dependencies.stack-up] workspace = true diff --git a/crates/auth/auth.toml b/crates/auth/auth.toml index 4e5b263..17a696f 100644 --- a/crates/auth/auth.toml +++ b/crates/auth/auth.toml @@ -4,6 +4,7 @@ port = 1304 [misc.oauth] session-lifespan = 3600 # seconds +jwt-encoding-key = "secret" [misc.oauth.discord] # query param for provider diff --git a/crates/auth/migrations/20250723100947_user.sql b/crates/auth/migrations/20250723100947_user.sql index 440afc7..b5566fe 100644 --- a/crates/auth/migrations/20250723100947_user.sql +++ b/crates/auth/migrations/20250723100947_user.sql @@ -2,8 +2,19 @@ create table auth_user ( id uuid primary key, email text unique not null, - avatar text, - description text, - updated_at text, - create_at timestamptz not null default now() + updated_at timestamptz not null default now(), + created_at timestamptz not null default now() ); + +create or replace function set_updated_at() +returns trigger as $$ +begin + new.updated_at := now(); + return new; +end; +$$ language plpgsql; + +create trigger trigger_set_updated_at +before update on auth_user +for each row +execute function set_updated_at(); diff --git a/crates/auth/migrations/20250725160900_session.sql b/crates/auth/migrations/20250725160900_session.sql new file mode 100644 index 0000000..c5e76dc --- /dev/null +++ b/crates/auth/migrations/20250725160900_session.sql @@ -0,0 +1,8 @@ +-- Add migration script here +create schema if not exists "tower_sessions"; + +create table "tower_sessions"."session" ( + id text primary key not null, + data bytea not null, + expiry_date timestamptz not null +); diff --git a/crates/auth/migrations/20250725161014_token.sql b/crates/auth/migrations/20250725161014_token.sql new file mode 100644 index 0000000..68f476c --- /dev/null +++ b/crates/auth/migrations/20250725161014_token.sql @@ -0,0 +1,9 @@ +-- Add migration script here +create table token ( + user_id uuid not null, + token text not null, + session_id text not null, + primary key (user_id, session_id), + foreign key (session_id) references "tower_sessions"."session"(id) on delete cascade, + foreign key (user_id) references auth_user(id) on delete cascade +); diff --git a/crates/auth/src/cnfg.rs b/crates/auth/src/cnfg.rs index af7b0a0..c895d05 100644 --- a/crates/auth/src/cnfg.rs +++ b/crates/auth/src/cnfg.rs @@ -11,6 +11,7 @@ pub struct LocalConfig { pub struct OauthConfig { pub discord: OauthCredentials, pub session_lifespan: u64, + pub jwt_encoding_key: String, } #[derive(Deserialize, Clone)] diff --git a/crates/auth/src/server/routes.rs b/crates/auth/src/server/routes.rs index 1ab012c..6773962 100644 --- a/crates/auth/src/server/routes.rs +++ b/crates/auth/src/server/routes.rs @@ -1,5 +1,8 @@ pub mod authorised; pub mod discord; + +use std::fmt::Display; + use axum::response::IntoResponse; use serde::Deserialize; @@ -9,6 +12,18 @@ pub enum Provider { Discord, } +impl Display for Provider { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Provider::Discord => "discord", + } + ) + } +} + pub async fn health_check() -> impl IntoResponse { let name = env!("CARGO_PKG_NAME"); let ver = env!("CARGO_PKG_VERSION"); diff --git a/crates/auth/src/server/routes/authorised.rs b/crates/auth/src/server/routes/authorised.rs index d493db5..27f02bc 100644 --- a/crates/auth/src/server/routes/authorised.rs +++ b/crates/auth/src/server/routes/authorised.rs @@ -8,13 +8,15 @@ use axum::{ }; use axum_extra::{TypedHeader, headers}; use oauth2::{AuthorizationCode, TokenResponse}; -use reqwest::header::SET_COOKIE; +use reqwest::{StatusCode, header::SET_COOKIE}; use serde::{Deserialize, Serialize}; use sqlx::types::uuid; +use time::OffsetDateTime; use tower_sessions::{ SessionStore, session::{Id, Record}, }; +use uuid::Uuid; use crate::{ error::AppError, @@ -37,17 +39,38 @@ struct User { avatar: Option<String>, username: String, discriminator: String, + verified: bool, + email: String, +} + +#[derive(Debug, Deserialize, Serialize)] +struct DbUser { + id: Uuid, + email: String, + created_at: OffsetDateTime, + updated_at: OffsetDateTime, } /// The cookie to store the session id for user information. const SESSION_COOKIE: &str = "info"; const SESSION_DATA_KEY: &str = "data"; +#[derive(Debug, Serialize, Deserialize)] +struct Claims { + iss: String, + sub: Uuid, + exp: i64, + iat: i64, + sid: String, + aud: String, +} + pub async fn login_authorised( Query(query): Query<AuthRequest>, State(state): State<AppHandle>, TypedHeader(cookies): TypedHeader<headers::Cookie>, ) -> Result<impl IntoResponse, AppError> { + let provider = query.provider.to_string(); let oauth_session_id = Id::from_str( cookies .get(OAUTH_CSRF_COOKIE) @@ -66,19 +89,112 @@ pub async fn login_authorised( .await .context("failed in sending request request to authorisation server")?; - let user_data: User = client + let user_data = client // https://discord.com/developers/docs/resources/user#get-current-user .get("https://discordapp.com/api/users/@me") .bearer_auth(token.access_token().secret()) .send() .await .context("failed in sending request to target Url")? - .json::<User>() + .json::<serde_json::Value>() .await .context("failed to deserialise response as JSON")?; + let user_data: User = serde_json::from_value(user_data)?; + + if !user_data.verified { + return Ok((StatusCode::UNAUTHORIZED, "email is not verified").into_response()); + } + // Create a new session filled with user data let session_id = Id(i128::from_le_bytes(uuid::Uuid::new_v4().to_bytes_le())); + + let mut transaction = state.services.postgres.begin().await?; + + let user = sqlx::query_as!( + DbUser, + " + select + p.* + from + auth_user p + inner join + oauth_account a + on + p.id=a.user_id + where a.provider_id = $1 and a.provider_user_id = $2 + ", + provider, + user_data.id + ) + .fetch_optional(&mut *transaction) + .await?; + + let user = if let Some(user) = user { + println!("some"); + user + } else { + println!("none"); + let uuid = uuid::Uuid::now_v7(); + let user = sqlx::query_as!( + DbUser, + "insert into auth_user (id, email) values ($1, $2) + on conflict (email) do update + set email = excluded.email + returning *; + ", + uuid, + user_data.email, + // user_data.avatar.as_ref().map(|value| { + // format!( + // "https://cdn.discordapp.com/avatars/{}/{value}", + // user_data.id + // ) + // }) + ) + .fetch_one(&mut *transaction) + .await?; + + sqlx::query_as!( + DbUser, + "with upsert as ( + insert into oauth_account (provider_id, provider_user_id, user_id) values ($1, $2, $3) + on conflict (provider_id, provider_user_id) do update + set provider_id = excluded.provider_id -- no-op + returning user_id + ) + select u.* + from upsert + join auth_user u on u.id = upsert.user_id; + ", + provider, + user_data.id, + user.id + ) + .fetch_one(&mut *transaction) + .await? + }; + + let exp = OffsetDateTime::now_utc() + Duration::from_secs(15 * 60); + + let claims = Claims { + sub: user.id, + // Mandatory expiry time as UTC timestamp + exp: exp.unix_timestamp(), + iss: "sellershut".to_owned(), + sid: session_id.to_string(), + aud: "sellershut".to_owned(), + iat: OffsetDateTime::now_utc().unix_timestamp(), + }; + + let token = jsonwebtoken::encode( + &jsonwebtoken::Header::default(), + &claims, + &jsonwebtoken::EncodingKey::from_secret( + state.local_config.oauth.jwt_encoding_key.as_bytes(), + ), + )?; + store .create(&mut Record { id: session_id, @@ -93,15 +209,24 @@ pub async fn login_authorised( .await .context("failed in inserting serialised value into session")?; - // Store session and get corresponding cookie. + sqlx::query!( + "insert into token (user_id, token, session_id) values ($1, $2, $3)", + user.id, + token, + session_id.to_string() + ) + .execute(&mut *transaction) + .await?; + let cookie = format!("{SESSION_COOKIE}={session_id}; SameSite=Lax; HttpOnly; Secure; Path=/"); - // Set cookie let mut headers = HeaderMap::new(); headers.insert( SET_COOKIE, cookie.parse().context("failed to parse cookie")?, ); - Ok((headers, Redirect::to("/"))) + transaction.commit().await?; + + Ok((headers, Redirect::to("/")).into_response()) } |