use axum::{
body::Body,
extract::State,
http::{Request, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
};
use rusqlite::params;
use tracing::{info, warn};
use api_keys_simplified::{
ApiKeyManagerV0, Environment, ExposeSecret, KeyStatus, SecureString,
};
use crate::db::DbPool;
#[derive(Clone)]
pub struct AuthState {
pub db: DbPool,
pub manager: ApiKeyManagerV0,
}
pub fn create_key_manager() -> Result<ApiKeyManagerV0, String> {
ApiKeyManagerV0::init_default_config("lific_sk")
.map_err(|e| format!("failed to init key manager: {e}"))
}
pub fn create_api_key(
db: &DbPool,
manager: &ApiKeyManagerV0,
name: &str,
) -> Result<String, crate::error::LificError> {
let conn = db.write()?;
let exists: bool = conn
.query_row(
"SELECT COUNT(*) > 0 FROM api_keys WHERE name = ?1 AND revoked = 0",
params![name],
|row| row.get(0),
)
.unwrap_or(false);
if exists {
return Err(crate::error::LificError::BadRequest(format!(
"an active key named '{name}' already exists"
)));
}
let api_key = manager
.generate(Environment::production())
.map_err(|e| crate::error::LificError::Internal(format!("key generation failed: {e}")))?;
let plaintext = api_key.key().expose_secret().to_string();
let hash = api_key.expose_hash().hash().to_string();
conn.execute(
"INSERT INTO api_keys (name, key_hash) VALUES (?1, ?2)",
params![name, hash],
)?;
Ok(plaintext)
}
pub fn list_api_keys(db: &DbPool) -> Result<Vec<ApiKeyInfo>, crate::error::LificError> {
let conn = db.read()?;
let mut stmt = conn.prepare(
"SELECT id, name, created_at, expires_at, revoked FROM api_keys ORDER BY created_at",
)?;
let rows = stmt.query_map([], |row| {
Ok(ApiKeyInfo {
id: row.get(0)?,
name: row.get(1)?,
created_at: row.get(2)?,
expires_at: row.get(3)?,
revoked: row.get(4)?,
})
})?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| crate::error::LificError::Database(e))
}
pub fn revoke_api_key(db: &DbPool, name: &str) -> Result<(), crate::error::LificError> {
let conn = db.write()?;
let changed = conn.execute(
"UPDATE api_keys SET revoked = 1 WHERE name = ?1 AND revoked = 0",
params![name],
)?;
if changed == 0 {
return Err(crate::error::LificError::NotFound(format!(
"no active key named '{name}'"
)));
}
info!(name, "API key revoked");
Ok(())
}
pub fn rotate_api_key(
db: &DbPool,
manager: &ApiKeyManagerV0,
name: &str,
) -> Result<String, crate::error::LificError> {
let conn = db.write()?;
let changed = conn.execute(
"DELETE FROM api_keys WHERE name = ?1",
params![name],
)?;
if changed == 0 {
return Err(crate::error::LificError::NotFound(format!(
"no key named '{name}'"
)));
}
drop(conn);
create_api_key(db, manager, name)
}
pub fn has_any_keys(db: &DbPool) -> bool {
if let Ok(conn) = db.read() {
conn.query_row("SELECT COUNT(*) FROM api_keys", [], |row| row.get::<_, i64>(0))
.unwrap_or(0)
> 0
} else {
false
}
}
#[derive(Debug)]
pub struct ApiKeyInfo {
pub id: i64,
pub name: String,
pub created_at: String,
pub expires_at: Option<String>,
pub revoked: bool,
}
pub async fn require_api_key(
State(auth): State<AuthState>,
request: Request<Body>,
next: Next,
) -> Response {
let token = request
.headers()
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.map(|s| s.trim().to_string());
let Some(token) = token else {
return (
StatusCode::UNAUTHORIZED,
[("WWW-Authenticate", "Bearer")],
"Missing Authorization: Bearer <key> header",
)
.into_response();
};
let secure_token = SecureString::from(token);
let hashes: Vec<String> = {
let conn = match auth.db.read() {
Ok(c) => c,
Err(_) => {
return (StatusCode::INTERNAL_SERVER_ERROR, "database error").into_response()
}
};
let mut stmt = match conn
.prepare("SELECT key_hash FROM api_keys WHERE revoked = 0")
{
Ok(s) => s,
Err(_) => {
return (StatusCode::INTERNAL_SERVER_ERROR, "database error").into_response()
}
};
match stmt.query_map([], |row| row.get::<_, String>(0)) {
Ok(rows) => rows.filter_map(|r| r.ok()).collect(),
Err(_) => {
return (StatusCode::INTERNAL_SERVER_ERROR, "database error").into_response()
}
}
};
if hashes.is_empty() {
warn!("no API keys configured, allowing unauthenticated access");
return next.run(request).await;
}
for hash in &hashes {
match auth.manager.verify(&secure_token, hash) {
Ok(KeyStatus::Valid) => {
return next.run(request).await;
}
Ok(KeyStatus::Invalid) => continue,
Err(_) => continue,
}
}
warn!("rejected invalid API key");
(
StatusCode::UNAUTHORIZED,
[("WWW-Authenticate", "Bearer")],
"Invalid API key",
)
.into_response()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db;
use api_keys_simplified::SecureString;
fn test_db() -> db::DbPool { db::open_memory().expect("test db") }
#[test]
fn create_key_returns_valid_format() {
let pool = test_db();
let manager = create_key_manager().unwrap();
let key = create_api_key(&pool, &manager, "test-key").unwrap();
assert!(key.starts_with("lific_sk-live-"));
}
#[test]
fn verify_key_succeeds() {
let pool = test_db();
let manager = create_key_manager().unwrap();
let key = create_api_key(&pool, &manager, "test-key").unwrap();
let keys = list_api_keys(&pool).unwrap();
assert_eq!(keys.len(), 1);
let secure_key = SecureString::from(key);
let conn = pool.read().unwrap();
let hash: String = conn.query_row(
"SELECT key_hash FROM api_keys WHERE name = 'test-key'", [], |row| row.get(0),
).unwrap();
let status = manager.verify(&secure_key, &hash).unwrap();
assert!(matches!(status, KeyStatus::Valid));
}
#[test]
fn wrong_key_fails() {
let pool = test_db();
let manager = create_key_manager().unwrap();
create_api_key(&pool, &manager, "test-key").unwrap();
let conn = pool.read().unwrap();
let hash: String = conn.query_row(
"SELECT key_hash FROM api_keys WHERE name = 'test-key'", [], |row| row.get(0),
).unwrap();
let wrong_key = SecureString::from("lific_sk-live-AAAAAAAAAAAAAAAAAAAAAAAAAAAA.0000000000000000".to_string());
let status = manager.verify(&wrong_key, &hash);
match status {
Ok(KeyStatus::Valid) => panic!("wrong key should not validate"),
_ => {} }
}
#[test]
fn revoke_key_works() {
let pool = test_db();
let manager = create_key_manager().unwrap();
create_api_key(&pool, &manager, "revoke-me").unwrap();
revoke_api_key(&pool, "revoke-me").unwrap();
let keys = list_api_keys(&pool).unwrap();
assert!(keys[0].revoked);
}
#[test]
fn rotate_key_replaces_old() {
let pool = test_db();
let manager = create_key_manager().unwrap();
let old_key = create_api_key(&pool, &manager, "rotate-me").unwrap();
let new_key = rotate_api_key(&pool, &manager, "rotate-me").unwrap();
assert_ne!(old_key, new_key);
assert!(new_key.starts_with("lific_sk-live-"));
let keys = list_api_keys(&pool).unwrap();
assert_eq!(keys.len(), 1);
assert!(!keys[0].revoked);
}
#[test]
fn duplicate_name_rejected() {
let pool = test_db();
let manager = create_key_manager().unwrap();
create_api_key(&pool, &manager, "unique").unwrap();
let result = create_api_key(&pool, &manager, "unique");
assert!(result.is_err());
}
#[test]
fn has_any_keys_works() {
let pool = test_db();
assert!(!has_any_keys(&pool));
let manager = create_key_manager().unwrap();
create_api_key(&pool, &manager, "first").unwrap();
assert!(has_any_keys(&pool));
}
}