summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGalen Guyer <galen@galenguyer.com>2022-06-03 10:27:39 -0400
committerGalen Guyer <galen@galenguyer.com>2022-06-03 10:29:24 -0400
commit375a93d48e7fe490a62c958dfde7a48939db5cff (patch)
tree6227231aff56d416ec6f6e283e9d04de5726cdc1
parentc596f05cfab5226ce7bf6e3ad4c0310c3e2a1981 (diff)
add api key auth
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml5
-rw-r--r--migrations/2022-04-09-create-schema.sql10
-rw-r--r--src/db/strings.rs7
-rw-r--r--src/db/users.rs11
-rw-r--r--src/extractors.rs40
-rw-r--r--src/main.rs7
-rw-r--r--src/routes/v1/features.rs1
8 files changed, 73 insertions, 15 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 652a0b6..d8ac00a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -332,7 +332,7 @@ checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71"
[[package]]
name = "fdns-api"
-version = "0.1.2"
+version = "0.1.3"
dependencies = [
"addr",
"anyhow",
@@ -340,6 +340,7 @@ dependencies = [
"bcrypt",
"chrono",
"dotenvy",
+ "hex",
"hmac",
"jwt",
"lazy_static",
@@ -1391,9 +1392,9 @@ dependencies = [
[[package]]
name = "totp-rs"
-version = "1.4.0"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "665c8ac1c4280d5e2deb982cf2ee8b90df0e86cf5234acaaef5b785cb1150040"
+checksum = "bb938fe0a13c97a1f7e6aab365f41bb2d6d3fdf84e9a97e0d0aaf4f90013dd80"
dependencies = [
"base32",
"constant_time_eq",
diff --git a/Cargo.toml b/Cargo.toml
index ff5ba70..da67d51 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "fdns-api"
-version = "0.1.2"
+version = "0.1.3"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -12,6 +12,7 @@ axum = "0.5.6"
bcrypt = "0.13.0"
chrono = { version = "0.4.19", features = ["serde"] }
dotenvy = "0.15.1"
+hex = "0.4.3"
hmac = "0.12.1"
jwt = "0.16.0"
lazy_static = "1.4.0"
@@ -23,7 +24,7 @@ serde_json = "1.0.81"
sha2 = "0.10.2"
sqlx = { version = "0.5.13", features = ["postgres", "runtime-tokio-rustls", "chrono", "uuid"] }
tokio = { version = "1.18.2", features = ["full"] }
-totp-rs = "1.4.0"
+totp-rs = "2.0.0"
tower = "0.4.12"
tower-http = { version = "0.3.3", features = ["cors", "trace"] }
tracing-subscriber = "0.3.11"
diff --git a/migrations/2022-04-09-create-schema.sql b/migrations/2022-04-09-create-schema.sql
index 89bfa3f..830ad4a 100644
--- a/migrations/2022-04-09-create-schema.sql
+++ b/migrations/2022-04-09-create-schema.sql
@@ -33,6 +33,16 @@ CREATE TABLE IF NOT EXISTS records (
constraint zone_id_fk foreign key (zone_id) references zones (id)
);
+CREATE TABLE IF NOT EXISTS api_keys (
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
+ owner_uuid uuid NOT NULL,
+ token_hash varchar(255) NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (now() AT TIME ZONE 'UTC'),
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
+ last_used TIMESTAMP WITH TIME ZONE,
+ constraint owner_uuid_fk foreign key (owner_uuid) references users (id)
+);
+
CREATE OR REPLACE FUNCTION update_modified_column()
RETURNS TRIGGER AS $$
BEGIN
diff --git a/src/db/strings.rs b/src/db/strings.rs
index 484aa3e..7a7a5cf 100644
--- a/src/db/strings.rs
+++ b/src/db/strings.rs
@@ -37,4 +37,11 @@ lazy_static! {
SELECT id,zone_id,name,type,content,ttl,created_at,modified_at
FROM records WHERE zone_id = $1
";
+ pub(crate) static ref GET_USER_FROM_API_KEY: &'static str = r"
+ SELECT users.id,email,password,display_name,users.created_at,modified_at,admin,enabled,totp_secret FROM api_keys
+ JOIN users
+ ON users.id = api_keys.owner_uuid
+ WHERE api_keys.token_hash = $1
+ AND api_keys.expires_at > (now() AT TIME ZONE 'UTC');
+ ";
}
diff --git a/src/db/users.rs b/src/db/users.rs
index 57aa6f5..4606d21 100644
--- a/src/db/users.rs
+++ b/src/db/users.rs
@@ -39,3 +39,14 @@ pub async fn create_user(
transaction.commit().await?;
Ok(user)
}
+
+pub async fn get_user_from_api_key(
+ pool: &Pool<Postgres>,
+ token_hash: &str,
+) -> Result<User, sqlx::Error> {
+ let user = sqlx::query_as::<_, User>(&strings::GET_USER_FROM_API_KEY)
+ .bind(token_hash)
+ .fetch_one(pool)
+ .await?;
+ Ok(user)
+}
diff --git a/src/extractors.rs b/src/extractors.rs
index 9f77794..9e70266 100644
--- a/src/extractors.rs
+++ b/src/extractors.rs
@@ -13,13 +13,15 @@ use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::Value;
-use sha2::Sha256;
+use sha2::{Digest, Sha256};
use sqlx::types::Uuid;
+use sqlx::{Pool, Postgres};
use std::collections::BTreeMap;
use std::env;
use std::error::Error;
use std::net::IpAddr;
use std::net::SocketAddr;
+use std::sync::Arc;
lazy_static! {
static ref JWT_SECRET: String = env::var("JWT_SECRET").unwrap();
@@ -46,7 +48,7 @@ where
B::Data: Send,
B::Error: Into<BoxError>,
{
- type Rejection = (axum::http::StatusCode, axum::Json<serde_json::Value>);
+ type Rejection = (axum::http::StatusCode, Json<serde_json::Value>);
async fn from_request(
req: &mut axum::extract::RequestParts<B>,
@@ -61,12 +63,40 @@ where
Some(header) => {
let key: Hmac<Sha256> = Hmac::new_from_slice((*JWT_SECRET).as_bytes()).unwrap();
let token = header.replace("Bearer ", "");
+ if token.starts_with("fdns_") {
+ let mut hasher = Sha256::new();
+ hasher.update(token.as_bytes());
+ let digest = hasher.finalize();
+ let hash = hex::encode(digest);
+
+ let db_pool = req.extensions().get::<Arc<Pool<Postgres>>>().unwrap();
+
+ let user = match crate::db::users::get_user_from_api_key(db_pool, &hash).await {
+ Ok(user) => user,
+ Err(err) => {
+ return Err((
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": err.to_string()})),
+ ))
+ }
+ };
+ let token = Token {
+ iss: "fdns".to_owned(),
+ sub: user.id,
+ iat: 0,
+ exp: 0,
+ dn: user.display_name.unwrap_or_else(|| "".to_owned()),
+ email: user.email.to_owned(),
+ admin: user.admin,
+ };
+ return Ok(Self(token))
+ }
let claims: BTreeMap<String, String> = match token.verify_with_key(&key) {
Ok(claims) => claims,
Err(_) => {
return Err((
StatusCode::UNAUTHORIZED,
- axum::Json(json!({ "error": "Invalid token" })),
+ Json(json!({ "error": "Invalid token" })),
))
}
};
@@ -85,7 +115,7 @@ where
if token.iat > now || token.exp < now {
return Err((
StatusCode::UNAUTHORIZED,
- axum::Json(json!({"error": "Invalid token"})),
+ Json(json!({"error": "Invalid token"})),
));
}
@@ -94,7 +124,7 @@ where
None => {
return Err((
StatusCode::UNAUTHORIZED,
- axum::Json(json!({"error": "missing auth header"})),
+ Json(json!({"error": "missing auth header"})),
))
}
}
diff --git a/src/main.rs b/src/main.rs
index 997e693..bc144f2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -85,11 +85,8 @@ async fn main() {
),
),
)
- .layer(
- ServiceBuilder::new()
- .layer(TraceLayer::new_for_http())
- .layer(Extension(pg_pool)),
- );
+ .layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
+ .layer(Extension(pg_pool));
let addr = SocketAddr::from(([0, 0, 0, 0], 8000));
info!("Binding to {addr}");
diff --git a/src/routes/v1/features.rs b/src/routes/v1/features.rs
index 6d62c1b..ea48abc 100644
--- a/src/routes/v1/features.rs
+++ b/src/routes/v1/features.rs
@@ -7,6 +7,7 @@ pub async fn get_features() -> impl IntoResponse {
(
StatusCode::OK,
Json(json!({
+ "version": option_env!("CARGO_PKG_VERSION").unwrap_or_else(|| "unknown"),
"signup": *features::SIGNUPS_ENABLED,
"totp": *features::TOTP_ENABLED,
})),