mirror of
https://github.com/MedzikUser/HomeDisk.git
synced 2024-08-14 21:46:53 +00:00
feat (token): add jsonwebtoken
This commit is contained in:
parent
aa5cecc93c
commit
ac2eff6399
12 changed files with 260 additions and 26 deletions
88
Cargo.lock
generated
88
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -4,3 +4,6 @@ cors = [
|
|||
"127.0.0.1:8000",
|
||||
"localhost:8000",
|
||||
]
|
||||
|
||||
[jwt]
|
||||
secret = "secret key used to sign tokens"
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -26,4 +26,4 @@ features = ["full"]
|
|||
|
||||
[dependencies.homedisk-types]
|
||||
path = "../types"
|
||||
features = ["axum"]
|
||||
features = ["axum", "token"]
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
pub mod auth;
|
||||
pub mod errors;
|
||||
|
||||
#[cfg(feature = "token")]
|
||||
pub mod token;
|
||||
|
|
114
types/src/token.rs
Normal file
114
types/src/token.rs
Normal 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");
|
||||
}
|
||||
}
|
|
@ -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> {
|
||||
|
|
|
@ -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?)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue