Add authentication #1

Merged
hannaeko merged 11 commits from feature/auth into master 2021-04-03 17:54:16 +02:00
8 changed files with 114 additions and 61 deletions
Showing only changes of commit 759bf0cb4d - Show all commits

7
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@ -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<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>")]
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 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 }))
}

View File

@ -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<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 {
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() {
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();
}

View File

@ -15,7 +15,6 @@ pub struct ErrorResponse<T> {
pub details: Option<T>
}
#[derive(Serialize)]
#[serde(remote = "Status")]
struct StatusDef {
@ -24,7 +23,6 @@ struct StatusDef {
reason: &'static str,
}
impl<T> ErrorResponse<T> {
pub fn new(status: Status, message: String) -> ErrorResponse<T> {
ErrorResponse {
@ -46,7 +44,6 @@ impl<T> ErrorResponse<T> {
}
}
impl<'r, T: Serialize> Responder<'r> for ErrorResponse<T> {
fn respond_to(self, req: &Request) -> response::Result<'r> {
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);
ErrorResponse::new(Status::InternalServerError, "An unexpected error occured.".into())
}

View File

@ -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<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)]
pub struct UserInfo {
pub id: String,
@ -90,7 +116,6 @@ impl From<HasherError> for UserError {
}
}
impl LocalUser {
pub fn create_user(conn: &DbConn, user_request: CreateUserRequest) -> Result<UserInfo, UserError> {
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))
}
}