diff options
author | Galen Guyer <galen@galenguyer.com> | 2022-06-03 10:27:39 -0400 |
---|---|---|
committer | Galen Guyer <galen@galenguyer.com> | 2022-06-03 10:29:24 -0400 |
commit | 375a93d48e7fe490a62c958dfde7a48939db5cff (patch) | |
tree | 6227231aff56d416ec6f6e283e9d04de5726cdc1 | |
parent | c596f05cfab5226ce7bf6e3ad4c0310c3e2a1981 (diff) |
add api key auth
-rw-r--r-- | Cargo.lock | 7 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | migrations/2022-04-09-create-schema.sql | 10 | ||||
-rw-r--r-- | src/db/strings.rs | 7 | ||||
-rw-r--r-- | src/db/users.rs | 11 | ||||
-rw-r--r-- | src/extractors.rs | 40 | ||||
-rw-r--r-- | src/main.rs | 7 | ||||
-rw-r--r-- | src/routes/v1/features.rs | 1 |
8 files changed, 73 insertions, 15 deletions
@@ -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", @@ -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, })), |