feat (token): add jsonwebtoken
This commit is contained in:
parent
aa5cecc93c
commit
ac2eff6399
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,4 +26,4 @@ features = ["full"]
|
||||||
|
|
||||||
[dependencies.homedisk-types]
|
[dependencies.homedisk-types]
|
||||||
path = "../types"
|
path = "../types"
|
||||||
features = ["axum"]
|
features = ["axum", "token"]
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
|
|
||||||
|
#[cfg(feature = "token")]
|
||||||
|
pub mod token;
|
||||||
|
|
|
@ -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;
|
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> {
|
||||||
|
|
|
@ -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?)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue