#[cfg(feature = "local_auth")]
mod queries;
use base64::{display::Base64Display, engine::GeneralPurpose, Engine};
use chrono::{DateTime, Utc};
#[cfg(feature = "local_auth")]
pub use queries::*;
use serde::Deserialize;
use sha3::Digest;
use uuid::Uuid;
use super::{AuthError, OrganizationId, UserId};
#[derive(Clone, Debug, sqlx::FromRow)]
pub struct ApiKey {
pub api_key_id: Uuid,
pub organization_id: OrganizationId,
pub user_id: Option<UserId>,
pub inherits_user_permissions: bool,
pub description: String,
pub active: bool,
pub expires_at: DateTime<Utc>,
}
#[derive(Clone, Debug, Deserialize, sqlx::FromRow)]
pub struct ApiKeyUpdateBody {
pub description: Option<String>,
pub active: Option<bool>,
}
pub struct ApiKeyData {
pub api_key_id: Uuid,
pub hash: Vec<u8>,
pub key: String,
}
const B64_ENGINE: GeneralPurpose = base64::engine::general_purpose::URL_SAFE_NO_PAD;
impl ApiKeyData {
pub fn new() -> ApiKeyData {
let id = Uuid::now_v7();
let base64_id = Base64Display::new(id.as_bytes(), &B64_ENGINE);
let random_id = Uuid::new_v4();
let random = Base64Display::new(random_id.as_bytes(), &B64_ENGINE);
let key = format!("{base64_id}.{random}");
let hash = hash_key(&key);
ApiKeyData {
api_key_id: id,
key,
hash,
}
}
}
fn hash_key(key: &str) -> Vec<u8> {
let mut hasher = sha3::Sha3_512::default();
hasher.update(key.as_bytes());
hasher.finalize().to_vec()
}
pub fn decode_key(key: &str) -> Result<(Uuid, Vec<u8>), AuthError> {
if key.len() != 45 {
return Err(AuthError::ApiKeyFormat);
}
let hash = hash_key(key);
let id_portion = key.split_once('.').ok_or(AuthError::InvalidApiKey)?.0;
let api_key_bytes = B64_ENGINE
.decode(id_portion.as_bytes())
.map_err(|_| AuthError::InvalidApiKey)?;
let api_key_id = Uuid::from_slice(&api_key_bytes).map_err(|_| AuthError::ApiKeyFormat)?;
Ok((api_key_id, hash))
}
#[cfg(feature = "local_auth")]
pub async fn lookup_api_key_from_bearer_token(
pool: &sqlx::PgPool,
key: &str,
) -> Result<ApiKey, error_stack::Report<AuthError>> {
let (api_key_id, hash) = decode_key(key)?;
queries::lookup_api_key_for_auth(pool, &api_key_id, &hash).await
}
#[cfg(test)]
mod tests {
use super::{decode_key, ApiKeyData};
#[test]
fn valid_key() {
let data = ApiKeyData::new();
let (api_key_id, hash) = decode_key(&data.key).expect("decoding key");
assert_eq!(api_key_id, data.api_key_id, "api_key_id");
assert_eq!(hash, data.hash, "hash");
}
#[test]
fn bad_key() {
let data = ApiKeyData::new();
let mut key = data.key;
key.pop();
key.push('a');
let (api_key_id, hash) = decode_key(&key).expect("decoding key");
assert_eq!(api_key_id, data.api_key_id, "api_key_id");
assert_ne!(hash, data.hash, "hash");
}
#[test]
fn bad_length() {
let data = ApiKeyData::new();
let mut key = String::from(&data.key);
key.push('a');
decode_key(&key).expect_err("length too high");
key.pop();
key.pop();
decode_key(&key).expect_err("length too low");
}
}