From ac2eff6399eecfeed8bd2984c144b2cbe23dd167 Mon Sep 17 00:00:00 2001 From: MedzikUser Date: Tue, 19 Apr 2022 15:14:17 +0200 Subject: [PATCH] feat (token): add jsonwebtoken --- Cargo.lock | 88 ++++++++++++++++++++++++++- config.toml | 3 + core/src/main.rs | 2 +- server/Cargo.toml | 2 +- server/src/auth/register.rs | 25 +++++--- server/src/lib.rs | 12 +++- types/Cargo.toml | 12 ++++ types/src/auth/login.rs | 5 +- types/src/lib.rs | 3 + types/src/token.rs | 114 +++++++++++++++++++++++++++++++++++ utils/src/config/parser.rs | 10 ++- utils/src/database/sqlite.rs | 10 +-- 12 files changed, 260 insertions(+), 26 deletions(-) create mode 100644 types/src/token.rs diff --git a/Cargo.lock b/Cargo.lock index 902ba43..98193ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,7 +190,7 @@ dependencies = [ "libc", "num-integer", "num-traits", - "time", + "time 0.1.43", "winapi", ] @@ -545,8 +545,11 @@ name = "homedisk-types" version = "0.0.0" dependencies = [ "axum", + "chrono", + "jsonwebtoken", "serde", "thiserror", + "uuid", ] [[package]] @@ -684,6 +687,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "8.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9051c17f81bae79440afa041b3a278e1de71bfb96d32454b477fd4703ccb6f" +dependencies = [ + "base64", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -817,6 +834,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -846,6 +874,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.27.1" @@ -915,6 +952,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +[[package]] +name = "pem" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947" +dependencies = [ + "base64", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -974,6 +1020,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "rand", +] + [[package]] name = "quote" version = "1.0.18" @@ -1178,6 +1233,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a762b1c38b9b990c694b9c2f8abe3372ce6a9ceaae6bca39cfc46e054f45745" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time 0.3.9", +] + [[package]] name = "simplelog" version = "0.11.2" @@ -1414,6 +1481,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +dependencies = [ + "itoa", + "libc", + "num_threads", + "quickcheck", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + [[package]] name = "tinyvec" version = "1.5.1" diff --git a/config.toml b/config.toml index ce2426b..e1064a4 100644 --- a/config.toml +++ b/config.toml @@ -4,3 +4,6 @@ cors = [ "127.0.0.1:8000", "localhost:8000", ] + +[jwt] +secret = "secret key used to sign tokens" diff --git a/core/src/main.rs b/core/src/main.rs index 9fb02b0..92a2e61 100644 --- a/core/src/main.rs +++ b/core/src/main.rs @@ -20,7 +20,7 @@ async fn main() { .map(|e| e.parse().expect("parse CORS host")) .collect(); - homedisk_server::serve(config.http.host, origins, db) + homedisk_server::serve(config.http.host.clone(), origins, db, config) .await .expect("start http server"); } diff --git a/server/Cargo.toml b/server/Cargo.toml index dad30dc..d6393f6 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -26,4 +26,4 @@ features = ["full"] [dependencies.homedisk-types] path = "../types" -features = ["axum"] +features = ["axum", "token"] diff --git a/server/src/auth/register.rs b/server/src/auth/register.rs index 4205e24..74db2a4 100644 --- a/server/src/auth/register.rs +++ b/server/src/auth/register.rs @@ -2,21 +2,28 @@ use axum::{Extension, Json}; use homedisk_types::{ auth::login::{Request, Response}, errors::{AuthError, ServerError}, + token::{Claims, Token}, +}; +use homedisk_utils::{ + config::Config, + database::{Database, User}, }; -use homedisk_utils::database::{Database, User}; pub async fn handle( db: Extension, + config: Extension, Json(request): Json, ) -> Result, ServerError> { - let response = match db - .create_user(User::new(&request.username, &request.password)) - .await - { - Ok(_) => Response::LoggedIn { - access_token: "access_token".to_string(), - refresh_token: "refresh_token".to_string(), - }, + let user = User::new(&request.username, &request.password); + + let response = match db.create_user(&user).await { + Ok(_) => { + let token = Token::new(config.jwt.secret.as_bytes(), Claims::new(user.id)).unwrap(); + + Response::LoggedIn { + access_token: token.encoded, + } + } Err(e) => { if e.to_string().contains("UNIQUE constraint failed") { diff --git a/server/src/lib.rs b/server/src/lib.rs index 085b99f..39c75a9 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -2,7 +2,7 @@ pub mod auth; mod error; use axum::{http::HeaderValue, routing::get, Extension, Router, Server}; -use homedisk_utils::database::Database; +use homedisk_utils::{config::Config, database::Database}; use log::{debug, info}; use tower_http::cors::{CorsLayer, Origin}; @@ -10,7 +10,12 @@ async fn health_check() -> &'static str { "I'm alive!" } -pub async fn serve(host: String, origins: Vec, db: Database) -> error::Result<()> { +pub async fn serve( + host: String, + origins: Vec, + db: Database, + config: Config, +) -> error::Result<()> { debug!("starting http server"); info!("Website available at: http://{host}"); @@ -18,7 +23,8 @@ pub async fn serve(host: String, origins: Vec, db: Database) -> err .route("/health-check", get(health_check)) .nest("/auth", auth::app()) .layer(CorsLayer::new().allow_origin(Origin::list(origins))) - .layer(Extension(db)); + .layer(Extension(db)) + .layer(Extension(config)); Server::bind(&host.parse()?) .serve(app.into_make_service()) diff --git a/types/Cargo.toml b/types/Cargo.toml index cc021a2..51eea0d 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -3,8 +3,12 @@ name = "homedisk-types" version = "0.0.0" edition = "2021" +[features] +token = ["chrono", "jsonwebtoken"] + [dependencies] thiserror = "1.0.30" +uuid = "0.8.2" [dependencies.serde] version = "1.0.136" @@ -13,3 +17,11 @@ features = ["derive"] [dependencies.axum] version = "0.5.1" optional = true + +# token +[dependencies.chrono] +version = "0.4.19" +optional = true +[dependencies.jsonwebtoken] +version = "8.1.0" +optional = true diff --git a/types/src/auth/login.rs b/types/src/auth/login.rs index 8c33113..301d515 100644 --- a/types/src/auth/login.rs +++ b/types/src/auth/login.rs @@ -8,8 +8,5 @@ pub struct Request { #[derive(Debug, Serialize, Deserialize, Clone)] pub enum Response { - LoggedIn { - access_token: String, - refresh_token: String, - }, + LoggedIn { access_token: String }, } diff --git a/types/src/lib.rs b/types/src/lib.rs index 2c1d8be..fa82f9d 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -1,2 +1,5 @@ pub mod auth; pub mod errors; + +#[cfg(feature = "token")] +pub mod token; diff --git a/types/src/token.rs b/types/src/token.rs new file mode 100644 index 0000000..4c9e807 --- /dev/null +++ b/types/src/token.rs @@ -0,0 +1,114 @@ +use chrono::{Duration, Utc}; +use jsonwebtoken::{ + decode, encode, errors::Error, Algorithm, DecodingKey, EncodingKey, Header, TokenData, + Validation, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub exp: i64, + pub iat: i64, +} + +impl Claims { + /// Generate Json Web Token Claims + /// ``` + /// use homedisk_types::token::Claims; + /// + /// let user_id = "123".to_string(); + /// let claims = Claims::new(user_id); + /// ``` + pub fn new(sub: String) -> Self { + let iat = Utc::now(); + let exp = iat + Duration::hours(24); + + Self { + sub, + iat: iat.timestamp(), + exp: exp.timestamp(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Token { + header: Header, + pub claims: Claims, + pub encoded: String, +} + +impl Token { + /// Generate new token + /// ```ignore + /// use homedisk_types::token::{Token, Claims}; + /// + /// let claims = Claims::new("user_id_1234".to_string()); + /// let token = Token::new(secret, claims)?; + /// ``` + pub fn new(key: &[u8], claims: Claims) -> Result { + let header = Header::new(Algorithm::HS256); + let encoded = encode(&header, &claims, &EncodingKey::from_secret(key))?; + + Ok(Self { + header, + claims, + encoded, + }) + } + + /// Validate token + /// ```ignore + /// use homedisk_types::token::{Token, Claims}; + /// + /// let token = Token::new(secret, claims)?; + /// let decoded = Token::decode(secret, token.encoded)?; + /// ``` + pub fn decode(key: &[u8], token: String) -> Result> { + decode::( + &token, + &DecodingKey::from_secret(key), + &Validation::default(), + ) + } +} + +pub type Result = std::result::Result; + +#[cfg(test)] +mod test { + use super::{Claims, Token}; + + fn gen_token(key: &[u8]) -> Token { + Token::new(key, Claims::new("test".to_string())).expect("generate token") + } + + #[test] + fn new_token() { + let key = b"secret"; + gen_token(key); + } + + #[test] + fn decode_token() { + let key = b"secret"; + let token = gen_token(key); + + let decoded = Token::decode(key, token.encoded).unwrap(); + + assert_eq!(decoded.claims, token.claims) + } + + #[test] + fn decode_token_invalid_token() { + let key = b"key"; + let token = gen_token(key); + + let other_key = b"other key"; + + let err = Token::decode(other_key, token.encoded).unwrap_err(); + + assert_eq!(err.to_string(), "InvalidSignature"); + } +} diff --git a/utils/src/config/parser.rs b/utils/src/config/parser.rs index 2590040..2d705d9 100644 --- a/utils/src/config/parser.rs +++ b/utils/src/config/parser.rs @@ -4,17 +4,23 @@ use serde::{Deserialize, Serialize}; use super::Error; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub http: ConfigHTTP, + pub jwt: ConfigJWT, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConfigHTTP { pub host: String, pub cors: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConfigJWT { + pub secret: String, +} + impl Config { /// parse configuration file pub fn parse() -> Result { diff --git a/utils/src/database/sqlite.rs b/utils/src/database/sqlite.rs index fd7f1cd..de03825 100644 --- a/utils/src/database/sqlite.rs +++ b/utils/src/database/sqlite.rs @@ -40,16 +40,16 @@ impl Database { /// db.conn.execute(sqlx::query(&fs::read_to_string("../tables.sql").unwrap())).await.unwrap(); /// /// let user = User::new("medzik", "SuperSecretPassword123"); - /// db.create_user(user).await.unwrap(); + /// db.create_user(&user).await.unwrap(); /// } /// ``` - pub async fn create_user(&self, user: user::User) -> Result { + pub async fn create_user(&self, user: &user::User) -> Result { debug!("creating user - {}", user.username); let query = sqlx::query("INSERT INTO user (id, username, password) VALUES (?, ?, ?)") - .bind(user.id) - .bind(user.username) - .bind(user.password); + .bind(&user.id) + .bind(&user.username) + .bind(&user.password); Ok(self.conn.execute(query).await?) }