feat (token): add jsonwebtoken

This commit is contained in:
MedzikUser 2022-04-19 15:14:17 +02:00
parent aa5cecc93c
commit ac2eff6399
No known key found for this signature in database
GPG Key ID: A5FAC1E185C112DB
12 changed files with 260 additions and 26 deletions

88
Cargo.lock generated
View File

@ -190,7 +190,7 @@ dependencies = [
"libc", "libc",
"num-integer", "num-integer",
"num-traits", "num-traits",
"time", "time 0.1.43",
"winapi", "winapi",
] ]
@ -545,8 +545,11 @@ name = "homedisk-types"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"axum", "axum",
"chrono",
"jsonwebtoken",
"serde", "serde",
"thiserror", "thiserror",
"uuid",
] ]
[[package]] [[package]]
@ -684,6 +687,20 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -817,6 +834,17 @@ dependencies = [
"winapi", "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]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.44" version = "0.1.44"
@ -846,6 +874,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "num_threads"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.27.1" version = "0.27.1"
@ -915,6 +952,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc"
[[package]]
name = "pem"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947"
dependencies = [
"base64",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.1.0" version = "2.1.0"
@ -974,6 +1020,15 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "quickcheck"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
dependencies = [
"rand",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.18" version = "1.0.18"
@ -1178,6 +1233,18 @@ dependencies = [
"libc", "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]] [[package]]
name = "simplelog" name = "simplelog"
version = "0.11.2" version = "0.11.2"
@ -1414,6 +1481,25 @@ dependencies = [
"winapi", "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]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.5.1" version = "1.5.1"

View File

@ -4,3 +4,6 @@ cors = [
"127.0.0.1:8000", "127.0.0.1:8000",
"localhost:8000", "localhost:8000",
] ]
[jwt]
secret = "secret key used to sign tokens"

View File

@ -20,7 +20,7 @@ async fn main() {
.map(|e| e.parse().expect("parse CORS host")) .map(|e| e.parse().expect("parse CORS host"))
.collect(); .collect();
homedisk_server::serve(config.http.host, origins, db) homedisk_server::serve(config.http.host.clone(), origins, db, config)
.await .await
.expect("start http server"); .expect("start http server");
} }

View File

@ -26,4 +26,4 @@ features = ["full"]
[dependencies.homedisk-types] [dependencies.homedisk-types]
path = "../types" path = "../types"
features = ["axum"] features = ["axum", "token"]

View File

@ -2,21 +2,28 @@ use axum::{Extension, Json};
use homedisk_types::{ use homedisk_types::{
auth::login::{Request, Response}, auth::login::{Request, Response},
errors::{AuthError, ServerError}, errors::{AuthError, ServerError},
token::{Claims, Token},
};
use homedisk_utils::{
config::Config,
database::{Database, User},
}; };
use homedisk_utils::database::{Database, User};
pub async fn handle( pub async fn handle(
db: Extension<Database>, db: Extension<Database>,
config: Extension<Config>,
Json(request): Json<Request>, Json(request): Json<Request>,
) -> Result<Json<Response>, ServerError> { ) -> Result<Json<Response>, ServerError> {
let response = match db let user = User::new(&request.username, &request.password);
.create_user(User::new(&request.username, &request.password))
.await let response = match db.create_user(&user).await {
{ Ok(_) => {
Ok(_) => Response::LoggedIn { let token = Token::new(config.jwt.secret.as_bytes(), Claims::new(user.id)).unwrap();
access_token: "access_token".to_string(),
refresh_token: "refresh_token".to_string(), Response::LoggedIn {
}, access_token: token.encoded,
}
}
Err(e) => { Err(e) => {
if e.to_string().contains("UNIQUE constraint failed") { if e.to_string().contains("UNIQUE constraint failed") {

View File

@ -2,7 +2,7 @@ pub mod auth;
mod error; mod error;
use axum::{http::HeaderValue, routing::get, Extension, Router, Server}; 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 log::{debug, info};
use tower_http::cors::{CorsLayer, Origin}; use tower_http::cors::{CorsLayer, Origin};
@ -10,7 +10,12 @@ async fn health_check() -> &'static str {
"I'm alive!" "I'm alive!"
} }
pub async fn serve(host: String, origins: Vec<HeaderValue>, db: Database) -> error::Result<()> { pub async fn serve(
host: String,
origins: Vec<HeaderValue>,
db: Database,
config: Config,
) -> error::Result<()> {
debug!("starting http server"); debug!("starting http server");
info!("Website available at: http://{host}"); info!("Website available at: http://{host}");
@ -18,7 +23,8 @@ pub async fn serve(host: String, origins: Vec<HeaderValue>, db: Database) -> err
.route("/health-check", get(health_check)) .route("/health-check", get(health_check))
.nest("/auth", auth::app()) .nest("/auth", auth::app())
.layer(CorsLayer::new().allow_origin(Origin::list(origins))) .layer(CorsLayer::new().allow_origin(Origin::list(origins)))
.layer(Extension(db)); .layer(Extension(db))
.layer(Extension(config));
Server::bind(&host.parse()?) Server::bind(&host.parse()?)
.serve(app.into_make_service()) .serve(app.into_make_service())

View File

@ -3,8 +3,12 @@ name = "homedisk-types"
version = "0.0.0" version = "0.0.0"
edition = "2021" edition = "2021"
[features]
token = ["chrono", "jsonwebtoken"]
[dependencies] [dependencies]
thiserror = "1.0.30" thiserror = "1.0.30"
uuid = "0.8.2"
[dependencies.serde] [dependencies.serde]
version = "1.0.136" version = "1.0.136"
@ -13,3 +17,11 @@ features = ["derive"]
[dependencies.axum] [dependencies.axum]
version = "0.5.1" version = "0.5.1"
optional = true optional = true
# token
[dependencies.chrono]
version = "0.4.19"
optional = true
[dependencies.jsonwebtoken]
version = "8.1.0"
optional = true

View File

@ -8,8 +8,5 @@ pub struct Request {
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Response { pub enum Response {
LoggedIn { LoggedIn { access_token: String },
access_token: String,
refresh_token: String,
},
} }

View File

@ -1,2 +1,5 @@
pub mod auth; pub mod auth;
pub mod errors; pub mod errors;
#[cfg(feature = "token")]
pub mod token;

114
types/src/token.rs Normal file
View File

@ -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<Self> {
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<TokenData<Claims>> {
decode::<Claims>(
&token,
&DecodingKey::from_secret(key),
&Validation::default(),
)
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[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");
}
}

View File

@ -4,17 +4,23 @@ use serde::{Deserialize, Serialize};
use super::Error; use super::Error;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config { pub struct Config {
pub http: ConfigHTTP, pub http: ConfigHTTP,
pub jwt: ConfigJWT,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigHTTP { pub struct ConfigHTTP {
pub host: String, pub host: String,
pub cors: Vec<String>, pub cors: Vec<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigJWT {
pub secret: String,
}
impl Config { impl Config {
/// parse configuration file /// parse configuration file
pub fn parse() -> Result<Config, Error> { pub fn parse() -> Result<Config, Error> {

View File

@ -40,16 +40,16 @@ impl Database {
/// db.conn.execute(sqlx::query(&fs::read_to_string("../tables.sql").unwrap())).await.unwrap(); /// db.conn.execute(sqlx::query(&fs::read_to_string("../tables.sql").unwrap())).await.unwrap();
/// ///
/// let user = User::new("medzik", "SuperSecretPassword123"); /// 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<SqliteQueryResult, Error> { pub async fn create_user(&self, user: &user::User) -> Result<SqliteQueryResult, Error> {
debug!("creating user - {}", user.username); debug!("creating user - {}", user.username);
let query = sqlx::query("INSERT INTO user (id, username, password) VALUES (?, ?, ?)") let query = sqlx::query("INSERT INTO user (id, username, password) VALUES (?, ?, ?)")
.bind(user.id) .bind(&user.id)
.bind(user.username) .bind(&user.username)
.bind(user.password); .bind(&user.password);
Ok(self.conn.execute(query).await?) Ok(self.conn.execute(query).await?)
} }