summaryrefslogtreecommitdiffstats
path: root/crates/auth-service/src/server/routes/authorised.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/auth-service/src/server/routes/authorised.rs')
-rw-r--r--crates/auth-service/src/server/routes/authorised.rs236
1 files changed, 236 insertions, 0 deletions
diff --git a/crates/auth-service/src/server/routes/authorised.rs b/crates/auth-service/src/server/routes/authorised.rs
new file mode 100644
index 0000000..4d48299
--- /dev/null
+++ b/crates/auth-service/src/server/routes/authorised.rs
@@ -0,0 +1,236 @@
+use std::{str::FromStr, time::Duration};
+
+use anyhow::Context;
+use axum::{
+ extract::{Query, State},
+ http::HeaderMap,
+ response::{IntoResponse, Redirect},
+};
+use axum_extra::{TypedHeader, headers};
+use oauth2::{AuthorizationCode, TokenResponse};
+use reqwest::{StatusCode, header::SET_COOKIE};
+use sellershut_core::profile::CreateUserRequest;
+use serde::{Deserialize, Serialize};
+use sqlx::types::uuid;
+use time::OffsetDateTime;
+use tower_sessions::{
+ SessionStore,
+ session::{Id, Record},
+};
+use uuid::Uuid;
+
+use crate::{
+ auth::Claims,
+ error::AppError,
+ server::{
+ OAUTH_CSRF_COOKIE, csrf_token_validation::csrf_token_validation_workflow, routes::Provider,
+ },
+ state::AppHandle,
+};
+
+#[derive(Debug, Deserialize)]
+pub struct AuthRequest {
+ provider: Provider,
+ code: String,
+ pub state: String,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+struct User {
+ id: String,
+ 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";
+
+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)
+ .context("missing session cookie")?,
+ )?;
+ csrf_token_validation_workflow(&query, &state.session_store, oauth_session_id).await?;
+
+ let client = state.http_client.clone();
+ let store = state.session_store.clone();
+
+ // Get an auth token
+ let token = state
+ .discord_client
+ .exchange_code(AuthorizationCode::new(query.code.clone()))
+ .request_async(&client)
+ .await
+ .context("failed in sending request request to authorisation server")?;
+
+ 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::<serde_json::Value>()
+ .await
+ .context("failed to deserialise response as JSON")?;
+
+ dbg!(&user_data);
+
+ 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 {
+ user
+ } else {
+ 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,
+ )
+ .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,
+ 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(),
+ ),
+ )?;
+
+ let user_request = CreateUserRequest {
+ email: user_data.email.to_owned(),
+ avatar: user_data.avatar.as_ref().map(|value| {
+ format!(
+ "https://cdn.discordapp.com/avatars/{}/{value}",
+ user_data.id
+ )
+ }),
+ };
+
+ store
+ .create(&mut Record {
+ id: session_id,
+ data: [(
+ SESSION_DATA_KEY.to_string(),
+ serde_json::to_value(user_data).unwrap(),
+ )]
+ .into(),
+ expiry_date: time::OffsetDateTime::now_utc()
+ + Duration::from_secs(state.local_config.oauth.session_lifespan),
+ })
+ .await
+ .context("failed in inserting serialised value into session")?;
+
+ 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=/");
+
+ let mut profile_client = state.profile_client.clone();
+ let resp = profile_client.create_user(user_request).await?.into_inner();
+
+ let user_id = resp.temp_id;
+
+ let mut headers = HeaderMap::new();
+ headers.insert(
+ SET_COOKIE,
+ cookie.parse().context("failed to parse cookie")?,
+ );
+
+ transaction.commit().await?;
+
+ Ok((
+ headers,
+ Redirect::to(&format!("/?user={user_id}&token={token}")),
+ )
+ .into_response())
+}