Skip to main content

openapp_sdk_core/
auth.rs

1//! Authentication providers.
2//!
3//! The SDK ships a single [`TokenProvider`] trait so higher layers (bridge, Python
4//! wrappers) can plug in custom auth — session cookies, JWT, Vault-minted tokens —
5//! without breaking the rest of the client. The v1 default is [`StaticApiKey`], which
6//! sends the `OpenApp` API key in the [`API_KEY_HEADER`] header.
7//!
8//! The gateway (Oathkeeper) reads API keys only from `X-API-Key`; `Authorization:
9//! Bearer` is its JWT channel, so an API key sent there is never validated. See
10//! `notes/contracts/api-key-authentication.md`.
11
12use std::sync::Arc;
13
14use async_trait::async_trait;
15use openapp_sdk_common::ApiKey;
16use reqwest::header::{AUTHORIZATION, HeaderName};
17
18use crate::error::SdkError;
19
20/// Header the gateway reads `OpenApp` API keys from.
21pub const API_KEY_HEADER: HeaderName = HeaderName::from_static("x-api-key");
22
23/// Credentials returned by a [`TokenProvider`] for a single outgoing request: the
24/// header to set and its full value.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct AuthToken {
27    pub header: HeaderName,
28    pub value: String,
29}
30
31impl AuthToken {
32    /// An `OpenApp` API key, sent verbatim in [`API_KEY_HEADER`].
33    #[must_use]
34    pub fn api_key(token: impl Into<String>) -> Self {
35        Self {
36            header: API_KEY_HEADER,
37            value: token.into(),
38        }
39    }
40
41    /// A bearer credential (for example a JWT), sent as `Authorization: Bearer <token>`.
42    #[must_use]
43    pub fn bearer(token: impl AsRef<str>) -> Self {
44        Self {
45            header: AUTHORIZATION,
46            value: format!("Bearer {}", token.as_ref()),
47        }
48    }
49}
50
51/// Produces the credential header for every outgoing SDK request.
52#[async_trait]
53pub trait TokenProvider: Send + Sync + std::fmt::Debug {
54    /// Return the credentials to attach to the next request. May be called on the hot
55    /// path, so implementations should cache aggressively.
56    async fn token(&self) -> Result<AuthToken, SdkError>;
57}
58
59/// Shared-ownership handle used throughout the SDK.
60pub type SharedTokenProvider = Arc<dyn TokenProvider>;
61
62/// Static API-key provider: the token never changes for the lifetime of the client.
63#[derive(Debug, Clone)]
64pub struct StaticApiKey {
65    key: ApiKey,
66}
67
68impl StaticApiKey {
69    /// Wrap a parsed [`ApiKey`] into a provider.
70    #[must_use]
71    pub fn new(key: ApiKey) -> Self {
72        Self { key }
73    }
74
75    /// Parse and wrap a raw token string.
76    pub fn from_raw(token: impl Into<String>) -> Result<Self, SdkError> {
77        Ok(Self::new(ApiKey::parse(token)?))
78    }
79
80    /// Access the underlying [`ApiKey`] (e.g. to derive the base URL).
81    #[must_use]
82    pub fn api_key(&self) -> &ApiKey {
83        &self.key
84    }
85}
86
87#[async_trait]
88impl TokenProvider for StaticApiKey {
89    async fn token(&self) -> Result<AuthToken, SdkError> {
90        Ok(AuthToken::api_key(self.key.as_str()))
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[tokio::test]
99    async fn static_provider_emits_x_api_key_with_full_token() {
100        let provider = StaticApiKey::from_raw("https://openapp.house_openapp_SECRET").unwrap();
101        let token = provider.token().await.unwrap();
102        assert_eq!(token.header.as_str(), "x-api-key");
103        assert_eq!(token.value, "https://openapp.house_openapp_SECRET");
104    }
105
106    #[test]
107    fn bearer_token_uses_authorization_header() {
108        let token = AuthToken::bearer("jwt.payload.sig");
109        assert_eq!(token.header, AUTHORIZATION);
110        assert_eq!(token.value, "Bearer jwt.payload.sig");
111    }
112}