Skip to main content

siderite_core/security/
api_key.rs

1//! API keys carried in a header, query parameter or cookie.
2
3use super::document_scheme;
4use crate::error::ApiError;
5use crate::extract::FromRequestParts;
6use crate::header::Cookies;
7use http::StatusCode;
8use http::request::Parts;
9use serde_json::json;
10use siderite_openapi::{Operation, SchemaRegistry};
11use std::fmt;
12use std::marker::PhantomData;
13
14/// Where an API key is carried.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ApiKeyLocation {
17    /// A request header.
18    Header,
19    /// A query-string parameter.
20    Query,
21    /// A cookie.
22    Cookie,
23}
24
25impl ApiKeyLocation {
26    /// The OpenAPI `in` value.
27    const fn openapi(self) -> &'static str {
28        match self {
29            Self::Header => "header",
30            Self::Query => "query",
31            Self::Cookie => "cookie",
32        }
33    }
34}
35
36/// Describes one API-key scheme; implement on a marker type.
37pub trait ApiKeySpec: Send + Sync + 'static {
38    /// Header, query-parameter or cookie name.
39    const NAME: &'static str;
40    /// Where the key is carried.
41    const LOCATION: ApiKeyLocation;
42    /// Name under `components.securitySchemes`.
43    const SCHEME: &'static str;
44}
45
46/// An API key described by `S`.
47///
48/// A missing or empty key yields `401` (API keys have no standard
49/// `WWW-Authenticate` challenge, so none is sent). The key is not validated;
50/// compare it in constant time inside your own [`Authenticate`](super::Authenticate)
51/// implementation.
52pub struct ApiKey<S: ApiKeySpec> {
53    /// The raw key.
54    pub key: String,
55    spec: PhantomData<fn() -> S>,
56}
57
58impl<S: ApiKeySpec> ApiKey<S> {
59    /// Wrap a key value.
60    #[must_use]
61    pub fn new(key: impl Into<String>) -> Self {
62        Self {
63            key: key.into(),
64            spec: PhantomData,
65        }
66    }
67}
68
69impl<S: ApiKeySpec> fmt::Debug for ApiKey<S> {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        f.debug_struct("ApiKey")
72            .field("scheme", &S::SCHEME)
73            .field("key", &"[REDACTED]")
74            .finish()
75    }
76}
77
78impl<S: ApiKeySpec> Clone for ApiKey<S> {
79    fn clone(&self) -> Self {
80        Self::new(self.key.clone())
81    }
82}
83
84/// The first non-empty query value named `name`.
85fn query_value(parts: &Parts, name: &str) -> Option<String> {
86    let pairs = serde_urlencoded::from_str::<Vec<(String, String)>>(parts.uri.query()?).ok()?;
87    pairs
88        .into_iter()
89        .find(|(key, value)| key == name && !value.is_empty())
90        .map(|(_, value)| value)
91}
92
93/// The first non-empty header value named `name`.
94fn header_value(parts: &Parts, name: &str) -> Option<String> {
95    parts
96        .headers
97        .get_all(name)
98        .iter()
99        .filter_map(|value| value.to_str().ok())
100        .map(str::trim)
101        .find(|value| !value.is_empty())
102        .map(str::to_owned)
103}
104
105impl<S: ApiKeySpec> FromRequestParts for ApiKey<S> {
106    async fn from_request_parts(parts: &mut Parts) -> Result<Self, ApiError> {
107        let key = match S::LOCATION {
108            ApiKeyLocation::Header => header_value(parts, S::NAME),
109            ApiKeyLocation::Query => query_value(parts, S::NAME),
110            ApiKeyLocation::Cookie => {
111                let cookies = Cookies::from_request_parts(parts).await?;
112                cookies
113                    .get(S::NAME)
114                    .filter(|value| !value.is_empty())
115                    .map(str::to_owned)
116            }
117        };
118        key.map(Self::new)
119            .ok_or_else(|| ApiError::new(StatusCode::UNAUTHORIZED, "Not authenticated.").absent())
120    }
121
122    fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
123        document_scheme(
124            op,
125            registry,
126            S::SCHEME,
127            json!({"type": "apiKey", "in": S::LOCATION.openapi(), "name": S::NAME}),
128            &[],
129        );
130    }
131}