From f1f64665ccc3adda29dd44efb34b3f32e2a2726a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Sun, 24 Apr 2022 12:42:53 +0200 Subject: [PATCH] use stateful tokens --- Cargo.lock | 169 +----------------- Cargo.toml | 7 +- diesel.toml | 2 +- migrations/2022-04-24-085505_session/down.sql | 2 + migrations/2022-04-24-085505_session/up.sql | 8 + nomilo.example.toml | 2 - src/config.rs | 1 - src/models/auth.rs | 104 ++++++----- src/models/errors.rs | 4 +- src/models/mod.rs | 2 +- src/models/user.rs | 33 ++-- src/routes/users.rs | 19 +- src/schema.rs | 14 +- 13 files changed, 118 insertions(+), 249 deletions(-) create mode 100644 migrations/2022-04-24-085505_session/down.sql create mode 100644 migrations/2022-04-24-085505_session/up.sql diff --git a/Cargo.lock b/Cargo.lock index 6a1aee5..3cda004 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,12 +115,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - [[package]] name = "base64" version = "0.13.0" @@ -163,12 +157,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bumpalo" -version = "3.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" - [[package]] name = "byteorder" version = "1.4.3" @@ -262,7 +250,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" dependencies = [ "aes-gcm", - "base64 0.13.0", + "base64", "hkdf", "hmac", "percent-encoding", @@ -347,6 +335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b28135ecf6b7d446b43e27e225622a038cc4e2930a1022f51cdb97ada19b8e4d" dependencies = [ "byteorder", + "chrono", "diesel_derives", "libsqlite3-sys", "r2d2", @@ -754,29 +743,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" -[[package]] -name = "js-sys" -version = "0.3.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "jsonwebtoken" -version = "7.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afabcc15e437a6484fc4f12d0fd63068fe457bf93f1c148d3d9649c60b103f32" -dependencies = [ - "base64 0.12.3", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -918,7 +884,7 @@ dependencies = [ "log", "memchr", "mime", - "spin 0.9.3", + "spin", "tokio", "tokio-util 0.6.9", "version_check", @@ -938,7 +904,7 @@ name = "nomilo" version = "0.1.0-dev" dependencies = [ "argon2", - "base64 0.13.0", + "base64", "chrono", "clap", "diesel", @@ -946,13 +912,12 @@ dependencies = [ "diesel_migrations", "figment", "humantime", - "jsonwebtoken", + "rand", "rocket", "rocket_sync_db_pools", "serde", "serde_json", "tokio", - "toml", "trust-dns-client", "trust-dns-proto", "uuid", @@ -967,17 +932,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-bigint" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-integer" version = "0.1.44" @@ -1116,17 +1070,6 @@ dependencies = [ "syn", ] -[[package]] -name = "pem" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" -dependencies = [ - "base64 0.13.0", - "once_cell", - "regex", -] - [[package]] name = "percent-encoding" version = "2.1.0" @@ -1337,21 +1280,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted", - "web-sys", - "winapi", -] - [[package]] name = "rocket" version = "0.5.0-rc.1" @@ -1545,17 +1473,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simple_asn1" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b" -dependencies = [ - "chrono", - "num-bigint", - "num-traits", -] - [[package]] name = "slab" version = "0.4.6" @@ -1578,12 +1495,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.3" @@ -1985,12 +1896,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "url" version = "2.2.2" @@ -2053,70 +1958,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wasm-bindgen" -version = "0.2.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" -dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" - -[[package]] -name = "web-sys" -version = "0.3.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index e1f7cf0..d7c765a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,16 +13,15 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "6bdd2f8", version = "0.5.0-rc.1", features = ["json"] } rocket_sync_db_pools = { git = "https://github.com/SergioBenitez/Rocket", rev = "6bdd2f8", default-features = false, features = ["diesel_sqlite_pool"], version = "0.1.0-rc.1"} -toml = "0.5" base64 = "0.13.0" uuid = { version = "0.8.2", features = ["v4", "serde"] } -diesel = { version = "1.4", features = ["sqlite"] } +diesel = { version = "1.4", features = ["sqlite", "chrono"] } diesel_migrations = "1.4" diesel-derive-enum = { version = "1", features = ["sqlite"] } -jsonwebtoken = "7.2.0" chrono = { version = "0.4", features = ["serde"] } humantime = "2.1.0" tokio = "1" figment = { version = "0.10.6", features = ["toml", "env"] } clap = {version = "3", features = ["derive", "cargo"]} -argon2 = "0.4.0" +argon2 = {version = "0.4", default-features = false, features = ["alloc", "password-hash"] } +rand = "0.8" diff --git a/diesel.toml b/diesel.toml index 764d6e0..2fe6c9d 100644 --- a/diesel.toml +++ b/diesel.toml @@ -3,4 +3,4 @@ [print_schema] file = "src/schema.rs" -import_types = ["diesel::sql_types::*", "crate::models::users::*"] +import_types = ["diesel::sql_types::*", "crate::models::user::*", "crate::models::auth::*"] diff --git a/migrations/2022-04-24-085505_session/down.sql b/migrations/2022-04-24-085505_session/down.sql new file mode 100644 index 0000000..1f0a6d7 --- /dev/null +++ b/migrations/2022-04-24-085505_session/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE session; diff --git a/migrations/2022-04-24-085505_session/up.sql b/migrations/2022-04-24-085505_session/up.sql new file mode 100644 index 0000000..8668377 --- /dev/null +++ b/migrations/2022-04-24-085505_session/up.sql @@ -0,0 +1,8 @@ +-- Your SQL goes here +CREATE TABLE session ( + `session_id` VARCHAR NOT NULL, + `user_id` VARCHAR NOT NULL, + `expires_at` VARCHAR NOT NULL, + PRIMARY KEY(session_id), + FOREIGN KEY(user_id) REFERENCES user(id) +); diff --git a/nomilo.example.toml b/nomilo.example.toml index b32e08d..ca412fe 100644 --- a/nomilo.example.toml +++ b/nomilo.example.toml @@ -2,8 +2,6 @@ url = "db.sqlite" [release.web_app] -# base64 secret, change it (openssl rand -base64 32) -secret = "Y2hhbmdlbWUK" token_duration = "1d" [release.dns] diff --git a/src/config.rs b/src/config.rs index 2276ced..e922a91 100644 --- a/src/config.rs +++ b/src/config.rs @@ -23,7 +23,6 @@ pub struct DnsClientConfig { #[derive(Debug, Deserialize)] pub struct WebAppConfig { - pub secret: String, #[serde(deserialize_with = "from_duration")] pub token_duration: Duration, } diff --git a/src/models/auth.rs b/src/models/auth.rs index b0a63b7..288695b 100644 --- a/src/models/auth.rs +++ b/src/models/auth.rs @@ -1,63 +1,77 @@ -use uuid::Uuid; use serde::{Serialize, Deserialize}; -use chrono::serde::ts_seconds; -use chrono::prelude::{DateTime, Utc}; -use chrono::Duration; -use jsonwebtoken::{ - encode, decode, - Header, Validation, - Algorithm as JwtAlgorithm, EncodingKey, DecodingKey, - errors::Result as JwtResult -}; +use chrono::naive::serde::ts_seconds::serialize as ts_seconds_naive; +use chrono::{Duration, NaiveDateTime, Utc, DateTime}; + +use diesel::prelude::*; +use rand::Rng; +use rand::rngs::OsRng; +use rand::distributions::Alphanumeric; use crate::models::user::UserInfo; +use crate::schema::*; +use crate::models::errors::UserError; - -#[derive(Debug, Serialize, Deserialize)] -pub struct AuthClaims { - pub jti: String, - pub sub: String, - #[serde(with = "ts_seconds")] - pub exp: DateTime, - #[serde(with = "ts_seconds")] - pub iat: DateTime, -} - -#[derive(Debug, Serialize)] -pub struct AuthTokenResponse { - pub token: String -} - #[derive(Debug, Deserialize)] pub struct AuthTokenRequest { pub username: String, pub password: String, } -impl AuthClaims { - pub fn new(user_info: &UserInfo, token_duration: Duration) -> AuthClaims { - let jti = Uuid::new_v4().to_simple().to_string(); - let iat = Utc::now(); - let exp = iat + token_duration; +#[derive(Debug, Serialize, Queryable, Identifiable, Insertable)] +#[table_name = "session"] +#[primary_key(session_id)] +pub struct Session { + #[serde(rename = "token")] + pub session_id: String, + #[serde(skip)] + pub user_id: String, + #[serde(serialize_with = "ts_seconds_naive")] + pub expires_at: NaiveDateTime, +} - AuthClaims { - jti, - sub: user_info.id.clone(), - exp, - iat, - } +impl Session { + pub fn generate_id() -> String { + OsRng + .sample_iter(&Alphanumeric) + .take(50) + .map(char::from) + .collect() } - pub fn decode(token: &str, secret: &str) -> JwtResult { - decode::( - token, - &DecodingKey::from_secret(secret.as_ref()), - &Validation::new(JwtAlgorithm::HS256) - ).map(|data| data.claims) + pub fn from_session_id(conn: &diesel::SqliteConnection, id: &str) -> Result { + use crate::schema::session::dsl::*; + session + .find(id) + .get_result(conn) + .map_err(|_| UserError::ExpiredSession) + .and_then(|s: Session| { + let expires = DateTime::::from_utc(s.expires_at, Utc); + if expires < Utc::now() { + Err(UserError::ExpiredSession) + } else { + Ok(s) + } + }) } - pub fn encode(self, secret: &str) -> JwtResult { - encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_ref())) + pub fn new(conn: &diesel::SqliteConnection, user_info: &UserInfo, token_duration: Duration) -> Result { + use crate::schema::session::dsl::*; + + let expires = Utc::now() + token_duration; + + let user_session = Session { + session_id: Session::generate_id(), + user_id: user_info.id.clone(), + expires_at: expires.naive_utc(), + }; + + diesel::insert_into(session) + .values(&user_session) + .execute(conn) + .map_err(UserError::DbError)?; + + Ok(user_session) + } } diff --git a/src/models/errors.rs b/src/models/errors.rs index 09c7c61..576c98a 100644 --- a/src/models/errors.rs +++ b/src/models/errors.rs @@ -18,7 +18,7 @@ pub enum UserError { UserConflict, BadCreds, BadToken, - ExpiredToken, + ExpiredSession, MalformedHeader, PermissionDenied, DbError(DieselError), @@ -91,7 +91,7 @@ impl From for ErrorResponse { UserError::UserConflict => ErrorResponse::new(Status::Conflict, "This user already exists".into()), UserError::NotFound => ErrorResponse::new(Status::NotFound, "User does not exist".into()), UserError::BadToken => ErrorResponse::new(Status::BadRequest, "Malformed token".into()), - UserError::ExpiredToken => ErrorResponse::new(Status::Unauthorized, "The provided token has expired".into()), + UserError::ExpiredSession => ErrorResponse::new(Status::Unauthorized, "The provided session token has expired".into()), UserError::MalformedHeader => ErrorResponse::new(Status::BadRequest, "Malformed authorization header".into()), UserError::PermissionDenied => ErrorResponse::new(Status::Forbidden, "Bearer is not authorized to access the resource".into()), UserError::ZoneNotFound => ErrorResponse::new(Status::NotFound, "DNS zone does not exist".into()), diff --git a/src/models/mod.rs b/src/models/mod.rs index 5d1b2bd..d04ea25 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -9,7 +9,7 @@ pub mod user; pub mod zone; // Reexport types for convenience -pub use auth::{AuthClaims, AuthTokenRequest, AuthTokenResponse}; +pub use auth::{AuthTokenRequest, Session}; pub use class::DNSClass; pub use errors::{UserError, ErrorResponse, make_500}; pub use name::{AbsoluteName, SerdeName}; diff --git a/src/models/user.rs b/src/models/user.rs index b888bab..d47e894 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -2,23 +2,21 @@ use uuid::Uuid; use diesel::prelude::*; use diesel::result::Error as DieselError; use diesel_derive_enum::DbEnum; -use rocket::{State, request::{FromRequest, Request, Outcome}}; +use rocket::request::{FromRequest, Request, Outcome}; use rocket::outcome::try_outcome; use serde::{Deserialize}; use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use argon2::password_hash::errors::Error as PasswordHashError; -use argon2::password_hash::rand_core::OsRng; +use rand::rngs::OsRng; use argon2::{Algorithm, Argon2, Version, Params}; -use jsonwebtoken::{ - errors::ErrorKind as JwtErrorKind -}; + use crate::schema::*; use crate::DbConn; -use crate::config::Config; + use crate::models::errors::{UserError, ErrorResponse, make_500}; use crate::models::zone::Zone; -use crate::models::auth::AuthClaims; +use crate::models::auth::Session; const BEARER: &str = "Bearer "; @@ -127,31 +125,24 @@ impl<'r> FromRequest<'r> for UserInfo { }; let token = if auth_header.starts_with(BEARER) { - auth_header.trim_start_matches(BEARER) + auth_header.trim_start_matches(BEARER).to_string() } else { return ErrorResponse::from(UserError::MalformedHeader).into() }; - let config = try_outcome!(request.guard::<&State>().await.map_failure(make_500)); let conn = try_outcome!(request.guard::().await.map_failure(make_500)); - let token_data = AuthClaims::decode( - token, &config.web_app.secret - ).map_err(|e| match e.into_kind() { - JwtErrorKind::ExpiredSignature => UserError::ExpiredToken, - _ => UserError::BadToken, - }); + let session_res = conn.run(move |c| { + Session::from_session_id(c, &token) + }).await; - let token_data = match token_data { + let session = match session_res { Err(e) => return ErrorResponse::from(e).into(), - Ok(data) => data + Ok(s) => s, }; - let user_id = token_data.sub; - conn.run(move |c| { - match LocalUser::get_user_by_uuid(c, &user_id) { - Err(UserError::NotFound) => ErrorResponse::from(UserError::NotFound).into(), + match LocalUser::get_user_by_uuid(c, &session.user_id) { Err(e) => ErrorResponse::from(e).into(), Ok(d) => Outcome::Success(d), } diff --git a/src/routes/users.rs b/src/routes/users.rs index 66504b1..c770d23 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -12,17 +12,22 @@ pub async fn create_auth_token( conn: DbConn, config: &State, auth_request: Json -) -> Result, models::ErrorResponse> { +) -> Result, models::ErrorResponse> { - let user_info = conn.run(move |c| { - models::LocalUser::get_user_by_creds(c, &auth_request.username, &auth_request.password) + let session_duration = config.web_app.token_duration; + + let session = conn.run(move |c| { + let user_info = models::LocalUser::get_user_by_creds( + c, + &auth_request.username, + &auth_request.password + )?; + + models::Session::new(c, &user_info, session_duration) }).await?; - let token = models::AuthClaims::new(&user_info, config.web_app.token_duration) - .encode(&config.web_app.secret) - .map_err(models::make_500)?; - Ok(Json(models::AuthTokenResponse { token })) + Ok(Json(session)) } #[post("/users", data = "")] diff --git a/src/schema.rs b/src/schema.rs index 10c2d36..e6a0109 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,6 +1,6 @@ table! { use diesel::sql_types::*; - use crate::models::user::*; + use crate::models::user::RoleMapping; localuser (user_id) { user_id -> Text, @@ -10,6 +10,16 @@ table! { } } +table! { + use diesel::sql_types::*; + + session (session_id) { + session_id -> Text, + user_id -> Text, + expires_at -> Timestamp, + } +} + table! { use diesel::sql_types::*; @@ -37,11 +47,13 @@ table! { } joinable!(localuser -> user (user_id)); +joinable!(session -> user (user_id)); joinable!(user_zone -> user (user_id)); joinable!(user_zone -> zone (zone_id)); allow_tables_to_appear_in_same_query!( localuser, + session, user, user_zone, zone,