Add authentication #1
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 }))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue