add initial jwt code

This commit is contained in:
sam 2023-10-22 23:05:09 +02:00
parent dea8968f6b
commit 99246773ef
8 changed files with 218 additions and 20 deletions

124
Cargo.lock generated
View file

@ -462,6 +462,15 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "deranged"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -897,6 +906,7 @@ dependencies = [
"eyre", "eyre",
"handlebars", "handlebars",
"headers", "headers",
"jsonwebtoken",
"rust-embed", "rust-embed",
"serde", "serde",
"sqlx", "sqlx",
@ -955,6 +965,20 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "jsonwebtoken"
version = "9.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "155c4d7e39ad04c172c5e3a99c434ea3b4a7ba7960b38ecd562b270b097cce09"
dependencies = [
"base64",
"pem",
"ring 0.17.5",
"serde",
"serde_json",
"simple_asn1",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -1083,6 +1107,17 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-bigint"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-bigint-dig" name = "num-bigint-dig"
version = "0.8.4" version = "0.8.4"
@ -1191,6 +1226,16 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "pem"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3163d2912b7c3b52d651a055f2c7eec9ba5cd22d26ef75b8dd3a59980b185923"
dependencies = [
"base64",
"serde",
]
[[package]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.7.0" version = "0.7.0"
@ -1310,6 +1355,12 @@ version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -1421,11 +1472,25 @@ dependencies = [
"libc", "libc",
"once_cell", "once_cell",
"spin 0.5.2", "spin 0.5.2",
"untrusted", "untrusted 0.7.1",
"web-sys", "web-sys",
"winapi", "winapi",
] ]
[[package]]
name = "ring"
version = "0.17.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b"
dependencies = [
"cc",
"getrandom",
"libc",
"spin 0.9.8",
"untrusted 0.9.0",
"windows-sys",
]
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.2" version = "0.9.2"
@ -1508,7 +1573,7 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8"
dependencies = [ dependencies = [
"ring", "ring 0.16.20",
"rustls-webpki", "rustls-webpki",
"sct", "sct",
] ]
@ -1528,8 +1593,8 @@ version = "0.101.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe"
dependencies = [ dependencies = [
"ring", "ring 0.16.20",
"untrusted", "untrusted 0.7.1",
] ]
[[package]] [[package]]
@ -1565,8 +1630,8 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [ dependencies = [
"ring", "ring 0.16.20",
"untrusted", "untrusted 0.7.1",
] ]
[[package]] [[package]]
@ -1663,6 +1728,18 @@ dependencies = [
"rand_core", "rand_core",
] ]
[[package]]
name = "simple_asn1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085"
dependencies = [
"num-bigint",
"num-traits",
"thiserror",
"time",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@ -2030,6 +2107,35 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "time"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
dependencies = [
"deranged",
"itoa",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
dependencies = [
"time-core",
]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.6.0" version = "1.6.0"
@ -2246,6 +2352,12 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.4.1" version = "2.4.1"

View file

@ -14,6 +14,7 @@ dotenvy = "0.15.7"
eyre = "0.6.8" eyre = "0.6.8"
handlebars = { version = "4.4.0", features = ["rust-embed", "dir_source"] } handlebars = { version = "4.4.0", features = ["rust-embed", "dir_source"] }
headers = "0.3.9" headers = "0.3.9"
jsonwebtoken = "9.1.0"
rust-embed = "8.0.0" rust-embed = "8.0.0"
serde = { version = "1.0.189", features = ["derive"] } serde = { version = "1.0.189", features = ["derive"] }
sqlx = { version = "0.7.2", features = ["runtime-tokio", "tls-rustls", "postgres", "macros", "migrate", "chrono"] } sqlx = { version = "0.7.2", features = ["runtime-tokio", "tls-rustls", "postgres", "macros", "migrate", "chrono"] }

View file

@ -8,4 +8,6 @@ pub struct Config {
pub port: u16, pub port: u16,
#[arg(long, env, default_value_t = false)] #[arg(long, env, default_value_t = false)]
pub dev_mode: bool, pub dev_mode: bool,
#[arg(env)]
pub secret_key: String,
} }

View file

@ -7,7 +7,7 @@ use axum::{
use headers::Cookie; use headers::Cookie;
use tracing::error; use tracing::error;
use crate::{model::user::User, state::AppState}; use crate::{model::user::User, state::AppState, token::Claims};
pub struct ExtractUserToken(pub Option<User>); pub struct ExtractUserToken(pub Option<User>);
@ -33,17 +33,28 @@ where
Err(err) => { Err(err) => {
error!("Getting cookie header: {}", err); error!("Getting cookie header: {}", err);
return Ok(ExtractUserToken(None)); return Ok(ExtractUserToken(None));
}, }
}; };
let user = match cookie.get("imgboard-token") { match cookie.get("imgboard-token") {
Some(_token) => { Some(token) => {
// TODO: get from token let claims = Claims::decode(token, &state.decoding_key).map_err(|e| {
None error!("Decoding token claims: {}", e);
}, (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
None => None, })?;
};
Ok(ExtractUserToken(user)) match sqlx::query_as!(User, r#"SELECT * FROM users WHERE id = $1"#, claims.uid)
.fetch_one(&state.pool)
.await
{
Ok(u) => Ok(ExtractUserToken(Some(u))),
Err(err) => {
error!("Getting user from database: {}", err);
Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"))
}
}
}
None => Ok(ExtractUserToken(None)),
}
} }
} }

View file

@ -4,6 +4,7 @@ pub mod model;
pub mod pages; pub mod pages;
pub mod state; pub mod state;
pub mod templates; pub mod templates;
pub mod token;
use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc; use std::sync::Arc;
@ -11,6 +12,7 @@ use std::sync::Arc;
use axum::{routing::get, Router}; use axum::{routing::get, Router};
use clap::Parser; use clap::Parser;
use eyre::{Result, WrapErr}; use eyre::{Result, WrapErr};
use jsonwebtoken::{DecodingKey, EncodingKey};
use sqlx::migrate; use sqlx::migrate;
use sqlx::postgres::PgPoolOptions; use sqlx::postgres::PgPoolOptions;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
@ -44,14 +46,24 @@ async fn main() -> Result<()> {
migrate!().run(&pool).await.wrap_err("running migrations")?; migrate!().run(&pool).await.wrap_err("running migrations")?;
let state = Arc::new(AppState { pool, hbs }); let state = Arc::new(AppState {
pool,
hbs,
encoding_key: EncodingKey::from_base64_secret(&config.secret_key)
.wrap_err("parsing $SECRET_KEY")?,
decoding_key: DecodingKey::from_base64_secret(&config.secret_key)
.wrap_err("parsing $SECRET_KEY")?,
});
debug!("Building router"); debug!("Building router");
let app = Router::new() let app = Router::new()
.route("/", get(pages::index)) .route("/", get(pages::index))
.route("/users/:id", get(pages::get_user)) .route("/users/:id", get(pages::get_user))
.route("/users/new", get(pages::get_user_new).post(pages::post_user_new)) .route(
"/users/new",
get(pages::get_user_new).post(pages::post_user_new),
)
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
.with_state(state); .with_state(state);

View file

@ -13,7 +13,7 @@ pub struct User {
pub last_active: DateTime<Utc>, pub last_active: DateTime<Utc>,
} }
#[derive(sqlx::Type, Debug, Clone, PartialEq, Eq)] #[derive(sqlx::Type, Debug, Copy, Clone, PartialEq, Eq)]
#[repr(i32)] #[repr(i32)]
pub enum Role { pub enum Role {
Viewer = 1, Viewer = 1,
@ -62,3 +62,25 @@ impl TryFrom<&str> for Role {
} }
} }
} }
impl Into<&str> for Role {
fn into(self) -> &'static str {
match self {
Self::Viewer => "viewer",
Self::Editor => "editor",
Self::Manager => "manager",
Self::Admin => "admin",
}
}
}
impl Into<String> for Role {
fn into(self) -> String {
match self {
Self::Viewer => "viewer".into(),
Self::Editor => "editor".into(),
Self::Manager => "manager".into(),
Self::Admin => "admin".into(),
}
}
}

View file

@ -1,12 +1,19 @@
use std::sync::Arc; use std::sync::Arc;
use axum::{extract::{FromRef, FromRequestParts}, async_trait, http::request::Parts}; use axum::{
async_trait,
extract::{FromRef, FromRequestParts},
http::request::Parts,
};
use jsonwebtoken::{DecodingKey, EncodingKey};
use sqlx::{postgres::Postgres, Pool}; use sqlx::{postgres::Postgres, Pool};
#[derive(Clone, FromRef)] #[derive(Clone, FromRef)]
pub struct AppState { pub struct AppState {
pub pool: Pool<Postgres>, pub pool: Pool<Postgres>,
pub hbs: handlebars::Handlebars<'static>, pub hbs: handlebars::Handlebars<'static>,
pub encoding_key: EncodingKey,
pub decoding_key: DecodingKey,
} }
#[async_trait] #[async_trait]

31
src/token.rs Normal file
View file

@ -0,0 +1,31 @@
use jsonwebtoken::{errors::Error, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use crate::model::user::User;
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub exp: usize,
pub uid: i32,
pub role: String,
}
impl Claims {
pub fn new(user: &User) -> Self {
Self {
exp: 0,
uid: user.id,
role: user.role.into(),
}
}
pub fn encode(self, key: &EncodingKey) -> Result<String, Error> {
jsonwebtoken::encode(&Header::default(), &self, key)
}
pub fn decode(token: &str, key: &DecodingKey) -> Result<Self, Error> {
let token = jsonwebtoken::decode::<Claims>(token, key, &Validation::default())?;
Ok(token.claims)
}
}