Skip to main content

codoseo_web/agent/
auth.rs

1//! Who is calling the API: an API key in `Authorization: Bearer <key>`, and nothing else. The
2//! session cookie never authenticates the API (so no request can ride on a browser login), and a
3//! key that is malformed or revoked is refused, never treated as "no key".
4
5use axum::extract::FromRequestParts;
6use axum::http::HeaderMap;
7use axum::http::header::AUTHORIZATION;
8use axum::http::request::Parts;
9use codoseo_store::accounts::Account;
10use uuid::Uuid;
11
12use super::error::AgentError;
13use super::keys;
14use crate::metrics::{self, Surface, Tier};
15use crate::state::AppState;
16
17/// A caller holding a live API key: the key's account (as loaded for this request) and the key.
18#[derive(Debug, Clone)]
19pub struct ApiCaller {
20    pub account: Account,
21    pub key_id: Uuid,
22}
23
24const NO_KEY: &str = "Missing API key. Send it as the header \"Authorization: Bearer <key>\".";
25/// The same words for a malformed, an unknown and a revoked key, so nothing says which it was.
26const BAD_KEY: &str = "That API key is not valid, or it was revoked.";
27
28/// The key in the `Authorization` header: `Ok(None)` when the header is absent, an error when
29/// it is there but isn't a well-formed `Bearer` key.
30pub fn bearer_key(headers: &HeaderMap) -> Result<Option<&str>, AgentError> {
31    let Some(value) = headers.get(AUTHORIZATION) else {
32        return Ok(None);
33    };
34    let bad = || AgentError::Unauthorized(BAD_KEY.to_owned());
35    let value = value.to_str().map_err(|_| bad())?;
36    let (scheme, key) = value.split_once(' ').ok_or_else(bad)?;
37    if !scheme.eq_ignore_ascii_case("bearer") {
38        return Err(AgentError::Unauthorized(
39            "Use the header \"Authorization: Bearer <key>\".".to_owned(),
40        ));
41    }
42    let key = key.trim();
43    if keys::is_well_formed(key) {
44        Ok(Some(key))
45    } else {
46        Err(bad())
47    }
48}
49
50/// The caller behind a well-formed key: unknown and revoked keys are refused.
51pub async fn resolve_key(state: &AppState, key: &str) -> Result<ApiCaller, AgentError> {
52    match codoseo_store::api_keys::authenticate(&state.pool, &keys::hash_key(key)).await? {
53        Some((key_id, account)) => Ok(ApiCaller { account, key_id }),
54        None => Err(AgentError::Unauthorized(BAD_KEY.to_owned())),
55    }
56}
57
58/// The caller of a request that must carry a key.
59pub async fn authenticate(state: &AppState, headers: &HeaderMap) -> Result<ApiCaller, AgentError> {
60    match bearer_key(headers)? {
61        Some(key) => resolve_key(state, key).await,
62        None => Err(AgentError::Unauthorized(NO_KEY.to_owned())),
63    }
64}
65
66/// A handler argument that requires an API key. Rejects with the JSON 401.
67impl FromRequestParts<AppState> for ApiCaller {
68    type Rejection = AgentError;
69
70    async fn from_request_parts(
71        parts: &mut Parts,
72        state: &AppState,
73    ) -> Result<ApiCaller, AgentError> {
74        let caller = authenticate(state, &parts.headers).await;
75        // A refused key never reaches a handler, so this is where REST counts it.
76        if let Err(e) = &caller {
77            metrics::api_request(Surface::Rest, Tier::Key, e.code());
78        }
79        caller
80    }
81}