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",
"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"

View File

@ -4,3 +4,6 @@ cors = [
"127.0.0.1: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"))
.collect();
homedisk_server::serve(config.http.host, origins, db)
homedisk_server::serve(config.http.host.clone(), origins, db, config)
.await
.expect("start http server");
}

View File

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

View File

@ -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<Database>,
config: Extension<Config>,
Json(request): Json<Request>,
) -> Result<Json<Response>, 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") {

View File

@ -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<HeaderValue>, db: Database) -> error::Result<()> {
pub async fn serve(
host: String,
origins: Vec<HeaderValue>,
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<HeaderValue>, 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())

View File

@ -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

View File

@ -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 },
}

View File

@ -1,2 +1,5 @@
pub mod auth;
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;
#[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<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigJWT {
pub secret: String,
}
impl Config {
/// parse configuration file
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();
///
/// 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);
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?)
}