1use async_trait::async_trait;
4use http::{Request, Response, StatusCode, header::HeaderName};
5use rskit_errors::AppError;
6use std::sync::Arc;
7use tower::{Layer, Service};
8
9use super::Key;
10use crate::{AuthOutcome, MissingCredentialPolicy};
11
12#[async_trait]
14pub trait KeyValidator: Send + Sync {
15 async fn validate_key(&self, plain_key: &str) -> Result<Key, AppError>;
17}
18
19#[derive(Clone)]
25pub struct ApiKeyLayer<V> {
26 validator: Arc<V>,
27 header_name: HeaderName,
28 missing_policy: MissingCredentialPolicy,
29}
30
31impl<V: KeyValidator + 'static> ApiKeyLayer<V> {
32 pub fn new(validator: V) -> Self {
34 Self {
35 validator: Arc::new(validator),
36 header_name: HeaderName::from_static("x-api-key"),
37 missing_policy: MissingCredentialPolicy::RejectMissing,
38 }
39 }
40
41 #[must_use]
43 pub fn with_header(mut self, name: HeaderName) -> Self {
44 self.header_name = name;
45 self
46 }
47
48 #[must_use]
50 pub const fn accept_missing(mut self) -> Self {
51 self.missing_policy = MissingCredentialPolicy::AcceptMissing;
52 self
53 }
54}
55
56impl<S, V> Layer<S> for ApiKeyLayer<V>
57where
58 V: KeyValidator + 'static,
59{
60 type Service = ApiKeyService<S, V>;
61
62 fn layer(&self, inner: S) -> Self::Service {
63 ApiKeyService {
64 inner,
65 validator: Arc::clone(&self.validator),
66 header_name: self.header_name.clone(),
67 missing_policy: self.missing_policy,
68 }
69 }
70}
71
72#[derive(Clone)]
73pub struct ApiKeyService<S, V> {
74 inner: S,
75 validator: Arc<V>,
76 header_name: HeaderName,
77 missing_policy: MissingCredentialPolicy,
78}
79
80impl<S, V, ReqBody, ResBody> Service<Request<ReqBody>> for ApiKeyService<S, V>
81where
82 S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
83 S::Future: Send + 'static,
84 V: KeyValidator + 'static,
85 ReqBody: Send + 'static,
86 ResBody: Default + Send + 'static,
87{
88 type Response = S::Response;
89 type Error = S::Error;
90 type Future = std::pin::Pin<
91 Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
92 >;
93
94 fn poll_ready(
95 &mut self,
96 cx: &mut std::task::Context<'_>,
97 ) -> std::task::Poll<Result<(), Self::Error>> {
98 self.inner.poll_ready(cx)
99 }
100
101 fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
102 let clone = self.inner.clone();
103 let mut inner = std::mem::replace(&mut self.inner, clone);
104 let validator = Arc::clone(&self.validator);
105 let header_name = self.header_name.clone();
106 let missing_policy = self.missing_policy;
107
108 Box::pin(async move {
109 match extract_api_key(&req, &header_name) {
110 CredentialExtraction::Missing => {
111 if missing_policy == MissingCredentialPolicy::AcceptMissing {
112 let mut req = req;
113 req.extensions_mut().remove::<Key>();
114 req.extensions_mut().insert(AuthOutcome::<Key>::Missing);
115 inner.call(req).await
116 } else {
117 Ok(unauthorized_response())
118 }
119 }
120 CredentialExtraction::Invalid => Ok(unauthorized_response()),
121 CredentialExtraction::Present(plain_key) => {
122 if let Ok(key) = validator.validate_key(plain_key).await {
123 let mut req = req;
124 req.extensions_mut().insert(key.clone());
125 req.extensions_mut().insert(AuthOutcome::Authenticated(key));
126 inner.call(req).await
127 } else {
128 Ok(unauthorized_response())
129 }
130 }
131 }
132 })
133 }
134}
135
136enum CredentialExtraction<'a> {
137 Missing,
138 Invalid,
139 Present(&'a str),
140}
141
142fn extract_api_key<'a, B>(
143 req: &'a Request<B>,
144 header_name: &HeaderName,
145) -> CredentialExtraction<'a> {
146 let mut values = req.headers().get_all(header_name).iter();
147 let Some(value) = values.next() else {
148 return CredentialExtraction::Missing;
149 };
150 if values.next().is_some() {
151 return CredentialExtraction::Invalid;
152 }
153 let Ok(value) = value.to_str() else {
154 return CredentialExtraction::Invalid;
155 };
156 if value.trim().is_empty() || value.chars().any(char::is_whitespace) {
157 return CredentialExtraction::Invalid;
158 }
159 CredentialExtraction::Present(value)
160}
161
162fn unauthorized_response<ResBody: Default>() -> Response<ResBody> {
163 let mut response = Response::new(ResBody::default());
164 *response.status_mut() = StatusCode::UNAUTHORIZED;
165 response
166}
167
168#[cfg(test)]
169mod tests {
170 use std::{convert::Infallible, future::Ready};
171
172 use async_trait::async_trait;
173 use chrono::Utc;
174 use http::{Request, Response, StatusCode};
175 use rskit_errors::AppError;
176 use tower::{Layer, Service, ServiceExt};
177
178 use super::{ApiKeyLayer, CredentialExtraction, KeyValidator, extract_api_key};
179 use crate::{AuthOutcome, apikey::Key};
180
181 struct Validator;
182
183 #[async_trait]
184 impl KeyValidator for Validator {
185 async fn validate_key(&self, plain_key: &str) -> Result<Key, AppError> {
186 if plain_key == "pk.secret" {
187 Ok(Key {
188 id: "key-1".into(),
189 owner_id: "user-1".into(),
190 name: "primary".into(),
191 key_prefix: "pk".into(),
192 key_digest: "digest".into(),
193 scopes: vec!["read".into()],
194 is_active: true,
195 expires_at: None,
196 grace_ends_at: None,
197 rotated_by_id: None,
198 last_used_at: None,
199 created_at: Utc::now(),
200 })
201 } else {
202 Err(AppError::invalid_token())
203 }
204 }
205 }
206
207 #[derive(Clone)]
208 struct ExtensionCheckingService;
209
210 impl Service<Request<()>> for ExtensionCheckingService {
211 type Response = Response<()>;
212 type Error = Infallible;
213 type Future = Ready<Result<Self::Response, Self::Error>>;
214
215 fn poll_ready(
216 &mut self,
217 _cx: &mut std::task::Context<'_>,
218 ) -> std::task::Poll<Result<(), Self::Error>> {
219 std::task::Poll::Ready(Ok(()))
220 }
221
222 fn call(&mut self, request: Request<()>) -> Self::Future {
223 let status = if request.extensions().get::<Key>().is_some()
224 && request.extensions().get::<AuthOutcome<Key>>().is_some()
225 {
226 StatusCode::OK
227 } else if matches!(
228 request.extensions().get::<AuthOutcome<Key>>(),
229 Some(AuthOutcome::Missing)
230 ) {
231 StatusCode::NO_CONTENT
232 } else {
233 StatusCode::IM_A_TEAPOT
234 };
235 std::future::ready(Ok(Response::builder().status(status).body(()).unwrap()))
236 }
237 }
238
239 #[test]
240 fn api_key_extraction_requires_single_non_empty_header() {
241 let header = http::header::HeaderName::from_static("x-api-key");
242 let request = http::Request::builder()
243 .header(&header, "pk.secret")
244 .body(())
245 .unwrap();
246 assert!(matches!(
247 extract_api_key(&request, &header),
248 CredentialExtraction::Present("pk.secret")
249 ));
250
251 let request = http::Request::builder()
252 .header(&header, "one")
253 .header(&header, "two")
254 .body(())
255 .unwrap();
256 assert!(matches!(
257 extract_api_key(&request, &header),
258 CredentialExtraction::Invalid
259 ));
260 }
261
262 #[test]
263 fn api_key_extraction_rejects_blank_or_whitespace_values() {
264 let header = http::header::HeaderName::from_static("x-api-key");
265 let missing = http::Request::builder().body(()).unwrap();
266 assert!(matches!(
267 extract_api_key(&missing, &header),
268 CredentialExtraction::Missing
269 ));
270
271 for value in ["", " ", "pk.secret with-space"] {
272 let request = http::Request::builder()
273 .header(&header, value)
274 .body(())
275 .unwrap();
276 assert!(matches!(
277 extract_api_key(&request, &header),
278 CredentialExtraction::Invalid
279 ));
280 }
281 }
282
283 #[tokio::test]
284 async fn api_key_layer_rejects_missing_by_default_and_accepts_valid_keys() {
285 let mut service = ApiKeyLayer::new(Validator).layer(ExtensionCheckingService);
286
287 let missing = service
288 .ready()
289 .await
290 .unwrap()
291 .call(Request::builder().body(()).unwrap())
292 .await
293 .unwrap();
294 assert_eq!(missing.status(), StatusCode::UNAUTHORIZED);
295
296 let valid = service
297 .ready()
298 .await
299 .unwrap()
300 .call(
301 Request::builder()
302 .header("x-api-key", "pk.secret")
303 .body(())
304 .unwrap(),
305 )
306 .await
307 .unwrap();
308 assert_eq!(valid.status(), StatusCode::OK);
309 }
310
311 #[tokio::test]
312 async fn api_key_layer_uses_configured_header_name() {
313 let mut service = ApiKeyLayer::new(Validator)
314 .with_header(http::header::HeaderName::from_static("x-service-key"))
315 .layer(ExtensionCheckingService);
316
317 let default_header = service
318 .ready()
319 .await
320 .unwrap()
321 .call(
322 Request::builder()
323 .header("x-api-key", "pk.secret")
324 .body(())
325 .unwrap(),
326 )
327 .await
328 .unwrap();
329 assert_eq!(default_header.status(), StatusCode::UNAUTHORIZED);
330
331 let custom_header = service
332 .ready()
333 .await
334 .unwrap()
335 .call(
336 Request::builder()
337 .header("x-service-key", "pk.secret")
338 .body(())
339 .unwrap(),
340 )
341 .await
342 .unwrap();
343 assert_eq!(custom_header.status(), StatusCode::OK);
344 }
345
346 #[tokio::test]
347 async fn api_key_layer_accept_missing_does_not_accept_invalid_keys() {
348 let mut service = ApiKeyLayer::new(Validator)
349 .accept_missing()
350 .layer(ExtensionCheckingService);
351
352 let missing = service
353 .call(Request::builder().body(()).unwrap())
354 .await
355 .unwrap();
356 assert_eq!(missing.status(), StatusCode::NO_CONTENT);
357
358 let invalid = service
359 .call(
360 Request::builder()
361 .header("x-api-key", "pk.bad")
362 .body(())
363 .unwrap(),
364 )
365 .await
366 .unwrap();
367 assert_eq!(invalid.status(), StatusCode::UNAUTHORIZED);
368 }
369
370 #[tokio::test]
371 async fn api_key_layer_accept_missing_clears_stale_key() {
372 let mut service = ApiKeyLayer::new(Validator)
373 .accept_missing()
374 .layer(ExtensionCheckingService);
375 let mut request = Request::builder().body(()).unwrap();
376 request.extensions_mut().insert(Key {
377 id: "stale-key".into(),
378 owner_id: "user-1".into(),
379 name: "stale".into(),
380 key_prefix: "pk".into(),
381 key_digest: "digest".into(),
382 scopes: vec!["read".into()],
383 is_active: true,
384 expires_at: None,
385 grace_ends_at: None,
386 rotated_by_id: None,
387 last_used_at: None,
388 created_at: Utc::now(),
389 });
390
391 let response = service.call(request).await.unwrap();
392
393 assert_eq!(response.status(), StatusCode::NO_CONTENT);
394 }
395}