From cd28a75c8651b86a0660d68f75c873172356d5f9 Mon Sep 17 00:00:00 2001 From: Jai A Date: Sun, 27 Sep 2020 22:49:38 -0700 Subject: [PATCH] Authentication workflow complete, add database link --- .idea/sqldialects.xml | 3 + migrations/20200928020509_states.sql | 4 + migrations/20200928033759_edit-states.sql | 4 + .../20200928053955_make-url-not-null.sql | 3 + src/database/models/ids.rs | 11 ++ src/models/ids.rs | 2 +- src/routes/auth.rs | 129 ++++++++++++++++-- 7 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 migrations/20200928020509_states.sql create mode 100644 migrations/20200928033759_edit-states.sql create mode 100644 migrations/20200928053955_make-url-not-null.sql diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index b13859f4..a118a4e7 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -2,5 +2,8 @@ + + + \ No newline at end of file diff --git a/migrations/20200928020509_states.sql b/migrations/20200928020509_states.sql new file mode 100644 index 00000000..96e8e140 --- /dev/null +++ b/migrations/20200928020509_states.sql @@ -0,0 +1,4 @@ +CREATE TABLE states ( + id bigint PRIMARY KEY, + url varchar(500) +); \ No newline at end of file diff --git a/migrations/20200928033759_edit-states.sql b/migrations/20200928033759_edit-states.sql new file mode 100644 index 00000000..46fa2495 --- /dev/null +++ b/migrations/20200928033759_edit-states.sql @@ -0,0 +1,4 @@ +-- Add migration script here +ALTER TABLE states + +ADD COLUMN expires timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP + interval '1 hour'; \ No newline at end of file diff --git a/migrations/20200928053955_make-url-not-null.sql b/migrations/20200928053955_make-url-not-null.sql new file mode 100644 index 00000000..8649f57f --- /dev/null +++ b/migrations/20200928053955_make-url-not-null.sql @@ -0,0 +1,3 @@ +-- Add migration script here +ALTER TABLE states +ALTER COLUMN url SET NOT NULL; \ No newline at end of file diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index e2a3bacc..4265fb3e 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -73,6 +73,13 @@ generate_ids!( "SELECT EXISTS(SELECT 1 FROM team_members WHERE id=$1)", TeamMemberId ); +generate_ids!( + pub generate_state_id, + StateId, + 8, + "SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)", + StateId +); #[derive(Copy, Clone, Debug, Type)] #[sqlx(transparent)] @@ -109,6 +116,10 @@ pub struct CategoryId(pub i32); #[sqlx(transparent)] pub struct FileId(pub i64); +#[derive(Copy, Clone, Debug, Type)] +#[sqlx(transparent)] +pub struct StateId(pub i64); + use crate::models::ids; impl From for ModId { diff --git a/src/models/ids.rs b/src/models/ids.rs index f40585d6..46d287b3 100644 --- a/src/models/ids.rs +++ b/src/models/ids.rs @@ -169,7 +169,7 @@ pub mod base62_impl { output } - fn parse_base62(string: &str) -> Result { + pub fn parse_base62(string: &str) -> Result { let mut num: u64 = 0; for c in string.chars() { let next_digit; diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 80fdbd95..93906f5e 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,13 +1,23 @@ use crate::models::error::ApiError; use log::{info}; -use actix_web::web::{Query, ServiceConfig, scope}; +use actix_web::web::{Query, ServiceConfig, scope, Data}; use actix_web::{get, HttpResponse}; use actix_web::http::StatusCode; use serde::{Deserialize, Serialize}; use thiserror::Error; +use serde_json::Value; +use crate::database::models::generate_state_id; +use sqlx::postgres::PgPool; +use crate::models::ids::base62_impl::{to_base62, parse_base62}; +use chrono::Utc; +use crate::models::ids::{DecodingError}; pub fn config(cfg: &mut ServiceConfig) { - cfg.service(auth_callback); + cfg.service( + scope("/auth/") + .service(auth_callback) + .service(init) + ); } #[derive(Error, Debug)] @@ -22,8 +32,11 @@ pub enum AuthorizationError { SerDeError(#[from] serde_json::Error), #[error("Error while communicating to GitHub OAuth2")] GithubError(#[from] reqwest::Error), + #[error("Invalid Authentication credentials")] + InvalidCredentialsError, + #[error("Error while decoding Base62")] + DecodingError(#[from] DecodingError), } -// "https://github.com/login/oauth/authorize?client_id=3acffb2e808d16d4b226&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fapi%2Fv1%2Fauthcallback" impl actix_web::ResponseError for AuthorizationError { fn status_code(&self) -> StatusCode { match self { @@ -32,6 +45,8 @@ impl actix_web::ResponseError for AuthorizationError { AuthorizationError::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR, AuthorizationError::SerDeError(..) => StatusCode::BAD_REQUEST, AuthorizationError::GithubError(..) => StatusCode::FAILED_DEPENDENCY, + AuthorizationError::InvalidCredentialsError => StatusCode::UNAUTHORIZED, + AuthorizationError::DecodingError(..) => StatusCode::BAD_REQUEST, } } @@ -43,16 +58,23 @@ impl actix_web::ResponseError for AuthorizationError { AuthorizationError::DatabaseError(..) => "database_error", AuthorizationError::SerDeError(..) => "invalid_input", AuthorizationError::GithubError(..) => "github_error", + AuthorizationError::InvalidCredentialsError => "invalid_credentials", + AuthorizationError::DecodingError(..) => "decoding_error", }, description: &self.to_string(), }) } } +#[derive(Serialize, Deserialize)] +pub struct AuthorizationInit { + pub url: String, +} + #[derive(Serialize, Deserialize)] pub struct Authorization { pub code: String, - pub state: Option, + pub state: String, } #[derive(Serialize, Deserialize)] @@ -62,8 +84,80 @@ pub struct AccessToken { pub token_type: String, } -#[get("authcallback")] -pub async fn auth_callback(Query(info): Query) -> Result { +#[derive(Serialize, Deserialize)] +pub struct GitHubUser { + pub login: String, + pub id: usize, + pub node_id: String, + pub avatar_url: String, + pub gravatar_id: String, + pub url: String, + pub bio: String, +} + +//http://localhost:8000/api/v1/auth/init?url=https%3A%2F%2Fmodrinth.com%2Fmods +#[get("init")] +pub async fn init(Query(info): Query, client: Data) -> Result { + let mut transaction = client.begin().await?; + + let state = generate_state_id(&mut transaction).await?; + + sqlx::query!( + " + INSERT INTO states (id, url) + VALUES ($1, $2) + ", + state.0, + info.url + ) + .execute(&mut *transaction) + .await?; + + transaction.commit().await?; + + let client_id = dotenv::var("GITHUB_CLIENT_ID")?; + let url = format!("https://github.com/login/oauth/authorize?client_id={}&state={}&scope={}", client_id, to_base62(state.0 as u64), "%20repo%20read%3Aorg%20read%3Auser%20user%3Aemail"); + + Ok(HttpResponse::PermanentRedirect() + .header("Location", &*url) + .json(AuthorizationInit { + url, + })) +} + +#[get("callback")] +pub async fn auth_callback(Query(info): Query, client: Data) -> Result { + let mut transaction = client.begin().await?; + let state_id = parse_base62(&*info.state)?; + + let result = sqlx::query!( + " + SELECT url,expires FROM states + WHERE id = $1 + ", + state_id as i64 + ) + .fetch_one(&mut *transaction) + .await?; + + let now = Utc::now(); + let duration = result.expires.signed_duration_since(now); + + info!("{:?}", duration.num_seconds()); + if duration.num_seconds() < 0 { + return Err(AuthorizationError::InvalidCredentialsError); + } + + sqlx::query!( + " + DELETE FROM states + WHERE id = $1 + ", + state_id as i64 + ) + .execute(&mut *transaction) + .await?; + let client_id = dotenv::var("GITHUB_CLIENT_ID")?; let client_secret = dotenv::var("GITHUB_CLIENT_SECRET")?; @@ -72,7 +166,9 @@ pub async fn auth_callback(Query(info): Query) -> Result) -> Result