add config for tokens

main
Hannaeko 2021-03-27 13:23:19 -04:00
parent 2b8df138e0
commit 759bf0cb4d
8 changed files with 114 additions and 61 deletions

7
Cargo.lock generated
View File

@ -568,6 +568,12 @@ version = "1.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691" checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.10.16" version = "0.10.16"
@ -879,6 +885,7 @@ dependencies = [
"diesel", "diesel",
"diesel-derive-enum", "diesel-derive-enum",
"djangohashers", "djangohashers",
"humantime",
"jsonwebtoken", "jsonwebtoken",
"rocket", "rocket",
"rocket_contrib", "rocket_contrib",

View File

@ -21,3 +21,4 @@ diesel-derive-enum = { version = "1", features = ["sqlite"] }
djangohashers = { version = "1.4.0", features = ["with_argon2"], default-features = false } djangohashers = { version = "1.4.0", features = ["with_argon2"], default-features = false }
jsonwebtoken = "7.2.0" jsonwebtoken = "7.2.0"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
humantime = "2.1.0"

View File

@ -1,2 +1,7 @@
[dns_server] [web_app]
address = "127.0.0.1:53" # base64 secret, change it (openssl rand -base64 32)
secret = "Y2hhbmdlbWUK"
token_duration = "1d"
[dns]
server = "127.0.0.1:53"

View File

@ -1,55 +1,24 @@
use serde::{Serialize, Deserialize};
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use rocket::Response; use rocket::{Response, State};
use rocket::http::Status; use rocket::http::Status;
use uuid::Uuid;
use jsonwebtoken::{encode, Header, EncodingKey};
use chrono::prelude::{DateTime, Utc};
use chrono::Duration;
use chrono::serde::ts_seconds;
use crate::config::Config;
use crate::DbConn; use crate::DbConn;
use crate::models::errors::ErrorResponse; use crate::models::errors::{ErrorResponse, make_500};
use crate::models::users::{LocalUser, CreateUserRequest}; use crate::models::users::{LocalUser, CreateUserRequest, AuthClaims, AuthTokenRequest, AuthTokenResponse};
#[derive(Debug, Serialize, Deserialize)]
struct AuthClaims {
jti: String,
sub: String,
#[serde(with = "ts_seconds")]
exp: DateTime<Utc>,
#[serde(with = "ts_seconds")]
iat: DateTime<Utc>,
}
#[derive(Debug, Serialize)]
pub struct AuthTokenResponse {
token: String
}
#[derive(Debug, Deserialize)]
pub struct AuthTokenRequest {
username: String,
password: String,
}
#[post("/users/me/token", data = "<auth_request>")] #[post("/users/me/token", data = "<auth_request>")]
pub fn create_auth_token(conn: DbConn, auth_request: Json<AuthTokenRequest>) -> Result<Json<AuthTokenResponse>, ErrorResponse<()>> { pub fn create_auth_token(
conn: DbConn,
config: State<Config>,
auth_request: Json<AuthTokenRequest>
) -> Result<Json<AuthTokenResponse>, ErrorResponse<()>> {
let user_info = LocalUser::get_user_by_creds(&conn, &auth_request.username, &auth_request.password)?; let user_info = LocalUser::get_user_by_creds(&conn, &auth_request.username, &auth_request.password)?;
let jti = Uuid::new_v4().to_simple().to_string(); let token = AuthClaims::new(&user_info, config.web_app.token_duration)
let iat = Utc::now(); .encode(&config.web_app.secret)
let exp = iat + Duration::minutes(1); .map_err(|e| make_500(e))?;
let claims = AuthClaims {
jti: jti,
sub: user_info.id,
exp: exp,
iat: iat,
};
// TODO: catch error
let token = encode(&Header::default(), &claims, &EncodingKey::from_secret("changeme".as_ref())).unwrap();
Ok(Json(AuthTokenResponse { token })) Ok(Json(AuthTokenResponse { token }))
} }

View File

@ -2,20 +2,48 @@ use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
use std::fs; use std::fs;
use serde::{Deserialize}; use serde::{Deserialize, Deserializer};
use chrono::Duration;
use toml; use toml;
#[derive(Deserialize)] #[derive(Debug, Deserialize)]
pub struct Config { pub struct Config {
pub dns_server: DnsServerConfig pub dns: DnsConfig,
pub web_app: WebAppConfig,
} }
#[derive(Deserialize)] #[derive(Debug, Deserialize)]
pub struct DnsServerConfig { pub struct DnsConfig {
pub address: SocketAddr pub server: SocketAddr
}
#[derive(Debug, Deserialize)]
pub struct WebAppConfig {
#[serde(deserialize_with = "from_base64")]
pub secret: Vec<u8>,
#[serde(deserialize_with = "from_duration")]
pub token_duration: Duration,
}
fn from_base64<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where D: Deserializer<'de>
{
use serde::de::Error;
String::deserialize(deserializer)
.and_then(|string| base64::decode(&string).map_err(|err| Error::custom(err.to_string())))
}
fn from_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where D: Deserializer<'de>
{
use serde::de::Error;
String::deserialize(deserializer)
.and_then(|string| humantime::parse_duration(&string).map_err(|err| Error::custom(err.to_string())))
.and_then(|duration| Duration::from_std(duration).map_err(|err| Error::custom(err.to_string())))
} }
pub fn load(file_name: PathBuf) -> Config { pub fn load(file_name: PathBuf) -> Config {
toml::from_str(&fs::read_to_string(file_name).expect("could not read config file")).expect("could not parse config file") let file_content = fs::read_to_string(file_name).expect("could not read config file");
toml::from_str(&file_content).expect("could not parse config file")
} }

View File

@ -56,12 +56,14 @@ fn get_zone_records(client: State<SyncClient<TcpClientConnection>>, zone: String
fn main() { fn main() {
let app_config = config::load("config.toml".into()); let app_config = config::load("config.toml".into());
println!("{:#?}", app_config);
let conn = TcpClientConnection::new(app_config.dns_server.address).unwrap(); let conn = TcpClientConnection::new(app_config.dns.server).unwrap();
let client = SyncClient::new(conn); let client = SyncClient::new(conn);
rocket::ignite() rocket::ignite()
.manage(client) .manage(client)
.manage(app_config)
.attach(DbConn::fairing()) .attach(DbConn::fairing())
.mount("/api/v1", routes![get_zone_records, create_auth_token, create_user]).launch(); .mount("/api/v1", routes![get_zone_records, create_auth_token, create_user]).launch();
} }

View File

@ -15,7 +15,6 @@ pub struct ErrorResponse<T> {
pub details: Option<T> pub details: Option<T>
} }
#[derive(Serialize)] #[derive(Serialize)]
#[serde(remote = "Status")] #[serde(remote = "Status")]
struct StatusDef { struct StatusDef {
@ -24,7 +23,6 @@ struct StatusDef {
reason: &'static str, reason: &'static str,
} }
impl<T> ErrorResponse<T> { impl<T> ErrorResponse<T> {
pub fn new(status: Status, message: String) -> ErrorResponse<T> { pub fn new(status: Status, message: String) -> ErrorResponse<T> {
ErrorResponse { ErrorResponse {
@ -46,7 +44,6 @@ impl<T> ErrorResponse<T> {
} }
} }
impl<'r, T: Serialize> Responder<'r> for ErrorResponse<T> { impl<'r, T: Serialize> Responder<'r> for ErrorResponse<T> {
fn respond_to(self, req: &Request) -> response::Result<'r> { fn respond_to(self, req: &Request) -> response::Result<'r> {
let status = self.status; let status = self.status;
@ -65,7 +62,7 @@ impl From<UserError> for ErrorResponse<()> {
} }
} }
fn make_500<E: std::fmt::Debug>(e: E) -> ErrorResponse<()> { pub fn make_500<E: std::fmt::Debug>(e: E) -> ErrorResponse<()> {
println!("{:?}", e); println!("{:?}", e);
ErrorResponse::new(Status::InternalServerError, "An unexpected error occured.".into()) ErrorResponse::new(Status::InternalServerError, "An unexpected error occured.".into())
} }

View File

@ -1,15 +1,20 @@
use uuid::Uuid; use uuid::Uuid;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::result::Error as DieselError; use diesel::result::Error as DieselError;
use rocket::request::{FromRequest, Request, Outcome};
use diesel_derive_enum::DbEnum; use diesel_derive_enum::DbEnum;
use serde::Deserialize; use rocket::request::{FromRequest, Request, Outcome};
use serde::{Serialize, Deserialize};
use chrono::serde::ts_seconds;
use chrono::prelude::{DateTime, Utc};
use chrono::Duration;
// TODO: Maybe just use argon2 crate directly // TODO: Maybe just use argon2 crate directly
use djangohashers::{make_password_with_algorithm, check_password, HasherError, Algorithm}; use djangohashers::{make_password_with_algorithm, check_password, HasherError, Algorithm};
use jsonwebtoken::{encode, Header, EncodingKey, errors::Result as JwtResult};
use crate::schema::*; use crate::schema::*;
use crate::DbConn; use crate::DbConn;
#[derive(Debug, DbEnum, Deserialize)] #[derive(Debug, DbEnum, Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum Role { pub enum Role {
@ -48,6 +53,27 @@ pub struct CreateUserRequest {
// ldap_id: String // ldap_id: String
// } // }
#[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,
}
#[derive(Debug)] #[derive(Debug)]
pub struct UserInfo { pub struct UserInfo {
pub id: String, pub id: String,
@ -90,7 +116,6 @@ impl From<HasherError> for UserError {
} }
} }
impl LocalUser { impl LocalUser {
pub fn create_user(conn: &DbConn, user_request: CreateUserRequest) -> Result<UserInfo, UserError> { pub fn create_user(conn: &DbConn, user_request: CreateUserRequest) -> Result<UserInfo, UserError> {
use crate::schema::localuser::dsl::*; use crate::schema::localuser::dsl::*;
@ -160,3 +185,22 @@ impl LocalUser {
} }
} }
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;
AuthClaims {
jti: jti,
sub: user_info.id.clone(),
exp: exp,
iat: iat,
}
}
pub fn encode(self, secret: &[u8]) -> JwtResult<String> {
encode(&Header::default(), &self, &EncodingKey::from_secret(&secret))
}
}