Authentication workflow complete, add database link

This commit is contained in:
Jai A
2020-09-27 22:49:38 -07:00
parent 34075738ea
commit cd28a75c86
7 changed files with 147 additions and 9 deletions

3
.idea/sqldialects.xml generated
View File

@@ -2,5 +2,8 @@
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/migrations/20200716160921_init.sql" dialect="GenericSQL" />
<file url="file://$PROJECT_DIR$/migrations/20200812183213_unique-loaders.sql" dialect="GenericSQL" />
<file url="file://$PROJECT_DIR$/migrations/20200928033759_edit-states.sql" dialect="PostgreSQL" />
<file url="PROJECT" dialect="PostgreSQL" />
</component>
</project>

View File

@@ -0,0 +1,4 @@
CREATE TABLE states (
id bigint PRIMARY KEY,
url varchar(500)
);

View File

@@ -0,0 +1,4 @@
-- Add migration script here
ALTER TABLE states
ADD COLUMN expires timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP + interval '1 hour';

View File

@@ -0,0 +1,3 @@
-- Add migration script here
ALTER TABLE states
ALTER COLUMN url SET NOT NULL;

View File

@@ -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<ids::ModId> for ModId {

View File

@@ -169,7 +169,7 @@ pub mod base62_impl {
output
}
fn parse_base62(string: &str) -> Result<u64, DecodingError> {
pub fn parse_base62(string: &str) -> Result<u64, DecodingError> {
let mut num: u64 = 0;
for c in string.chars() {
let next_digit;

View File

@@ -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<String>,
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<Authorization>) -> Result<HttpResponse, AuthorizationError> {
#[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<AuthorizationInit>, client: Data<PgPool>) -> Result<HttpResponse, AuthorizationError> {
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<Authorization>, client: Data<PgPool>) -> Result<HttpResponse, AuthorizationError> {
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<Authorization>) -> Result<HttpResp
client_id, client_secret, info.code
);
let token : AccessToken = reqwest::Client::new()
let client = reqwest::Client::new();
let token : AccessToken = client
.post(&url)
.header(reqwest::header::ACCEPT, "application/json")
.send()
@@ -80,5 +176,22 @@ pub async fn auth_callback(Query(info): Query<Authorization>) -> Result<HttpResp
.json()
.await?;
Ok(HttpResponse::Ok().json(token))
let user : Value = client
.get("https://api.github.com/user")
.header(reqwest::header::USER_AGENT, "Modrinth")
.header(reqwest::header::AUTHORIZATION, format!("token {}", token.access_token))
.send()
.await?
.json()
.await?;
transaction.commit().await?;
let redirect_url = format!("{}?url={}", result.url, token.access_token);
Ok(HttpResponse::PermanentRedirect()
.header("Location", &*redirect_url)
.json(AuthorizationInit {
url: redirect_url,
}))
}