diff --git a/Cargo.lock b/Cargo.lock index 8609474..1646bf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3518,7 +3518,9 @@ dependencies = [ "rocket", "rocket_cors", "serde", + "substring", "tenebrous-rpc", + "thiserror", "tonic", "tracing-subscriber", ] diff --git a/api/Cargo.toml b/api/Cargo.toml index fbfc0ea..1672bf4 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -9,6 +9,8 @@ log = "0.4" tracing-subscriber = "0.2" tonic = { version = "0.4" } prost = "0.7" +thiserror = "1.0" +substring = "1.4" jsonwebtoken = "7.2" chrono = "0.4" serde = {version = "1.0", features = ["derive"] } diff --git a/api/src/api.rs b/api/src/api.rs index 7c18342..d91d99a 100644 --- a/api/src/api.rs +++ b/api/src/api.rs @@ -1,3 +1,4 @@ +use crate::auth::User; use crate::config::create_config; use crate::schema::{self, Context, Schema}; use log::info; @@ -26,7 +27,9 @@ async fn post_graphql_handler( context: &State, request: juniper_rocket_async::GraphQLRequest, schema: &State, + user: User, ) -> juniper_rocket_async::GraphQLResponse { + println!("User is {:?}", user); request.execute(&*schema, &*context).await } diff --git a/api/src/auth.rs b/api/src/auth.rs index f24b0d0..c29c0d1 100644 --- a/api/src/auth.rs +++ b/api/src/auth.rs @@ -1,14 +1,59 @@ use crate::config::Config; +use crate::errors::ApiError; use chrono::{Duration, Utc}; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; -use rocket::http::{Cookie, CookieJar}; +use rocket::request::{self, FromRequest, Request}; use rocket::response::status::Custom; use rocket::{ http::Status, serde::{json::Json, Deserialize, Serialize}, }; +use rocket::{ + http::{Cookie, CookieJar}, + outcome::Outcome, +}; use rocket::{routes, Route, State}; -use std::error::Error; +use substring::Substring; + +#[derive(Clone, Debug)] +pub(crate) struct User { + username: String, //TODO more state and such here. +} + +fn decode_token(token: &str, config: &Config) -> Result { + let token_data = decode::( + token, + &DecodingKey::from_secret(config.jwt_secret.as_bytes()), + &Validation::default(), + )?; + + Ok(token_data.claims) +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for User { + type Error = ApiError; + + async fn from_request(req: &'r Request<'_>) -> request::Outcome { + let config: Option<&Config> = req.rocket().state(); + let auth_header = req + .headers() + .get_one("Authorization") + .map(|auth| auth.substring("Bearer ".len(), auth.len())); + + let token = auth_header + .zip(config) + .map(|(encoded_token, app_cfg)| decode_token(encoded_token, app_cfg)) + .unwrap_or(Err(ApiError::AuthenticationDenied("username".to_string()))); + + match token { + Err(e) => Outcome::Failure((Status::Forbidden, e)), + Ok(token) => Outcome::Success(User { + username: token.sub, + }), + } + } +} pub(crate) fn routes() -> Vec { routes![login] diff --git a/api/src/errors.rs b/api/src/errors.rs new file mode 100644 index 0000000..9ae854b --- /dev/null +++ b/api/src/errors.rs @@ -0,0 +1,16 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ApiError { + #[error("user account does not exist: {0}")] + UserDoesNotExist(String), + + #[error("invalid password for user account: {0}")] + AuthenticationDenied(String), + + #[error("authentication token missing from request")] + AuthenticationRequired, + + #[error("error decoding token: {0}")] + TokenDecodingError(#[from] jsonwebtoken::errors::Error), +} diff --git a/api/src/lib.rs b/api/src/lib.rs index d2dd013..0de1d7d 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1,4 +1,5 @@ pub mod api; pub mod auth; pub mod config; +pub mod errors; pub mod schema;