diff --git a/Cargo.lock b/Cargo.lock index 46fdcd0..3fd27dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -568,6 +568,12 @@ version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.10.16" @@ -879,6 +885,7 @@ dependencies = [ "diesel", "diesel-derive-enum", "djangohashers", + "humantime", "jsonwebtoken", "rocket", "rocket_contrib", diff --git a/Cargo.toml b/Cargo.toml index 124b8a6..2712a07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,4 @@ diesel-derive-enum = { version = "1", features = ["sqlite"] } djangohashers = { version = "1.4.0", features = ["with_argon2"], default-features = false } jsonwebtoken = "7.2.0" chrono = { version = "0.4", features = ["serde"] } +humantime = "2.1.0" diff --git a/config.example.toml b/config.example.toml index fc23052..ade3716 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,2 +1,7 @@ -[dns_server] -address = "127.0.0.1:53" +[web_app] +# base64 secret, change it (openssl rand -base64 32) +secret = "Y2hhbmdlbWUK" +token_duration = "1d" + +[dns] +server = "127.0.0.1:53" diff --git a/src/auth/routes.rs b/src/auth/routes.rs index 247157b..d48c3cf 100644 --- a/src/auth/routes.rs +++ b/src/auth/routes.rs @@ -1,55 +1,24 @@ -use serde::{Serialize, Deserialize}; - use rocket_contrib::json::Json; -use rocket::Response; +use rocket::{Response, State}; 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::models::errors::ErrorResponse; -use crate::models::users::{LocalUser, CreateUserRequest}; +use crate::models::errors::{ErrorResponse, make_500}; +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, - #[serde(with = "ts_seconds")] - iat: DateTime, -} - -#[derive(Debug, Serialize)] -pub struct AuthTokenResponse { - token: String -} - -#[derive(Debug, Deserialize)] -pub struct AuthTokenRequest { - username: String, - password: String, -} #[post("/users/me/token", data = "")] -pub fn create_auth_token(conn: DbConn, auth_request: Json) -> Result, ErrorResponse<()>> { +pub fn create_auth_token( + conn: DbConn, + config: State, + auth_request: Json +) -> Result, ErrorResponse<()>> { + 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 iat = Utc::now(); - let exp = iat + Duration::minutes(1); - - 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(); + let token = AuthClaims::new(&user_info, config.web_app.token_duration) + .encode(&config.web_app.secret) + .map_err(|e| make_500(e))?; Ok(Json(AuthTokenResponse { token })) } diff --git a/src/config.rs b/src/config.rs index 8a1901d..f0e0428 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,20 +2,48 @@ use std::net::SocketAddr; use std::path::PathBuf; use std::fs; -use serde::{Deserialize}; +use serde::{Deserialize, Deserializer}; +use chrono::Duration; use toml; -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] pub struct Config { - pub dns_server: DnsServerConfig + pub dns: DnsConfig, + pub web_app: WebAppConfig, } -#[derive(Deserialize)] -pub struct DnsServerConfig { - pub address: SocketAddr +#[derive(Debug, Deserialize)] +pub struct DnsConfig { + pub server: SocketAddr +} + +#[derive(Debug, Deserialize)] +pub struct WebAppConfig { + #[serde(deserialize_with = "from_base64")] + pub secret: Vec, + #[serde(deserialize_with = "from_duration")] + pub token_duration: Duration, +} + +fn from_base64<'de, D>(deserializer: D) -> Result, 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 + 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 { - 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") } diff --git a/src/main.rs b/src/main.rs index d563a3f..6550319 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,12 +56,14 @@ fn get_zone_records(client: State>, zone: String fn main() { 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); rocket::ignite() .manage(client) + .manage(app_config) .attach(DbConn::fairing()) .mount("/api/v1", routes![get_zone_records, create_auth_token, create_user]).launch(); } diff --git a/src/models/errors.rs b/src/models/errors.rs index 856b28e..c8892f7 100644 --- a/src/models/errors.rs +++ b/src/models/errors.rs @@ -15,7 +15,6 @@ pub struct ErrorResponse { pub details: Option } - #[derive(Serialize)] #[serde(remote = "Status")] struct StatusDef { @@ -24,7 +23,6 @@ struct StatusDef { reason: &'static str, } - impl ErrorResponse { pub fn new(status: Status, message: String) -> ErrorResponse { ErrorResponse { @@ -46,7 +44,6 @@ impl ErrorResponse { } } - impl<'r, T: Serialize> Responder<'r> for ErrorResponse { fn respond_to(self, req: &Request) -> response::Result<'r> { let status = self.status; @@ -65,7 +62,7 @@ impl From for ErrorResponse<()> { } } -fn make_500(e: E) -> ErrorResponse<()> { +pub fn make_500(e: E) -> ErrorResponse<()> { println!("{:?}", e); ErrorResponse::new(Status::InternalServerError, "An unexpected error occured.".into()) } diff --git a/src/models/users.rs b/src/models/users.rs index 7772d51..cd0989b 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -1,15 +1,20 @@ use uuid::Uuid; use diesel::prelude::*; use diesel::result::Error as DieselError; -use rocket::request::{FromRequest, Request, Outcome}; 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 use djangohashers::{make_password_with_algorithm, check_password, HasherError, Algorithm}; +use jsonwebtoken::{encode, Header, EncodingKey, errors::Result as JwtResult}; use crate::schema::*; use crate::DbConn; + #[derive(Debug, DbEnum, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Role { @@ -48,6 +53,27 @@ pub struct CreateUserRequest { // ldap_id: String // } +#[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, +} + #[derive(Debug)] pub struct UserInfo { pub id: String, @@ -90,7 +116,6 @@ impl From for UserError { } } - impl LocalUser { pub fn create_user(conn: &DbConn, user_request: CreateUserRequest) -> Result { 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 { + encode(&Header::default(), &self, &EncodingKey::from_secret(&secret)) + } +}