use stateful tokens

main
Hannaeko 2022-04-24 12:42:53 +02:00
parent b70195bfe1
commit f1f64665cc
13 changed files with 118 additions and 249 deletions

169
Cargo.lock generated
View File

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

View File

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

View File

@ -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::*"]

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE session;

View File

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

View File

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

View File

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

View File

@ -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<Utc>,
#[serde(with = "ts_seconds")]
pub iat: DateTime<Utc>,
}
#[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<AuthClaims> {
decode::<AuthClaims>(
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<Session, UserError> {
use crate::schema::session::dsl::*;
session
.find(id)
.get_result(conn)
.map_err(|_| UserError::ExpiredSession)
.and_then(|s: Session| {
let expires = DateTime::<Utc>::from_utc(s.expires_at, Utc);
if expires < Utc::now() {
Err(UserError::ExpiredSession)
} else {
Ok(s)
}
})
}
pub fn encode(self, secret: &str) -> JwtResult<String> {
encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_ref()))
pub fn new(conn: &diesel::SqliteConnection, user_info: &UserInfo, token_duration: Duration) -> Result<Session, UserError> {
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)
}
}

View File

@ -18,7 +18,7 @@ pub enum UserError {
UserConflict,
BadCreds,
BadToken,
ExpiredToken,
ExpiredSession,
MalformedHeader,
PermissionDenied,
DbError(DieselError),
@ -91,7 +91,7 @@ impl From<UserError> 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()),

View File

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

View File

@ -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<Config>>().await.map_failure(make_500));
let conn = try_outcome!(request.guard::<DbConn>().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),
}

View File

@ -12,17 +12,22 @@ pub async fn create_auth_token(
conn: DbConn,
config: &State<Config>,
auth_request: Json<models::AuthTokenRequest>
) -> Result<Json<models::AuthTokenResponse>, models::ErrorResponse> {
) -> Result<Json<models::Session>, 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 = "<user_request>")]

View File

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