Skip to main content

ably_chat/
config.rs

1//! Static credentials for the client (ADR-0005).
2
3use std::sync::Arc;
4
5use base64::{Engine, engine::general_purpose::STANDARD};
6use futures::future::BoxFuture;
7
8use crate::error::Result;
9
10/// Supplies a currently-valid Bearer credential (Ably Token string or Ably JWT),
11/// refreshed on demand. The returned string is the raw token — the client adds
12/// the `Bearer ` prefix. Implementations MUST be cheap to call when cached.
13///
14/// An implementation MUST NOT call back into the same [`Client`](crate::Client)
15/// it authenticates in order to obtain its token: the client's cache mutex is
16/// non-reentrant and is held across this call, so doing so would deadlock.
17pub trait TokenProvider: Send + Sync {
18    /// Fetch a currently-valid token.
19    fn token(&self) -> BoxFuture<'_, Result<String>>;
20}
21
22/// Static credentials supplied to the client.
23///
24/// Ably accepts HTTP **Basic** auth with an API key (`keyName:keySecret`) or
25/// **Bearer** auth with an Ably Token/JWT. `Auth::Provider` refreshes on
26/// demand via a caller-supplied [`TokenProvider`] (ADR-0005).
27#[derive(Clone)]
28#[non_exhaustive]
29pub enum Auth {
30    /// An Ably API key, `keyName:keySecret`, sent as HTTP Basic.
31    ApiKey(String),
32    /// An Ably Token/JWT, sent as a Bearer token.
33    Token(String),
34    /// A caller-supplied provider that yields (and refreshes) Bearer credentials.
35    Provider(Arc<dyn TokenProvider>),
36}
37
38impl Auth {
39    /// Constructs API-key (HTTP Basic) credentials from a `keyName:keySecret`.
40    pub fn api_key(key: impl Into<String>) -> Self {
41        Auth::ApiKey(key.into())
42    }
43
44    /// Constructs Bearer-token credentials from an Ably Token/JWT.
45    pub fn token(token: impl Into<String>) -> Self {
46        Auth::Token(token.into())
47    }
48
49    /// Constructs credentials backed by a refreshing [`TokenProvider`].
50    pub fn provider(p: Arc<dyn TokenProvider>) -> Self {
51        Auth::Provider(p)
52    }
53
54    /// Renders the value for the `Authorization` header.
55    pub(crate) fn header_value(&self) -> String {
56        match self {
57            Auth::ApiKey(k) => format!("Basic {}", STANDARD.encode(k.as_bytes())),
58            Auth::Token(t) => format!("Bearer {t}"),
59            Auth::Provider(_) => unreachable!(
60                "Auth::Provider carries no static header; see AuthState::Provider in client.rs"
61            ),
62        }
63    }
64}
65
66impl std::fmt::Debug for Auth {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Auth::ApiKey(_) => f.write_str("Auth::ApiKey(<redacted>)"),
70            Auth::Token(_) => f.write_str("Auth::Token(<redacted>)"),
71            Auth::Provider(_) => f.write_str("Auth::Provider(<dyn TokenProvider>)"),
72        }
73    }
74}
75
76/// Splits a full Ably API key `appId.keyId:keySecret` into its name and secret.
77///
78/// Shared by the `jwt` and `token-issuance` features, which both need the halves
79/// separately (the key name serves as the JWT `kid` / HTTP Basic username, the
80/// secret as the signing key / Basic password).
81#[cfg(any(feature = "jwt", feature = "token-issuance"))]
82pub(crate) fn split_api_key(api_key: &str) -> crate::error::Result<(&str, &str)> {
83    let (name, secret) = api_key.split_once(':').ok_or_else(|| {
84        crate::error::Error::InvalidRequest("API key must be `keyName:keySecret`".into())
85    })?;
86    if name.is_empty() || secret.is_empty() {
87        return Err(crate::error::Error::InvalidRequest(
88            "API key name and secret must be non-empty".into(),
89        ));
90    }
91    Ok((name, secret))
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn api_key_becomes_basic_header() {
100        let a = Auth::api_key("app.key:secret");
101        assert_eq!(a.header_value(), "Basic YXBwLmtleTpzZWNyZXQ=");
102    }
103
104    #[test]
105    fn token_becomes_bearer_header() {
106        assert_eq!(Auth::token("tok123").header_value(), "Bearer tok123");
107    }
108
109    #[test]
110    fn debug_redacts_credentials() {
111        let dbg = format!("{:?}", Auth::api_key("app.key:secret"));
112        assert!(!dbg.contains("secret"));
113        let dbg = format!("{:?}", Auth::token("tok123"));
114        assert!(!dbg.contains("tok123"));
115    }
116
117    #[test]
118    fn provider_debug_redacts() {
119        use futures::future::BoxFuture;
120        use std::sync::Arc;
121
122        struct P;
123        impl crate::config::TokenProvider for P {
124            fn token(&self) -> BoxFuture<'_, crate::error::Result<String>> {
125                Box::pin(async { Ok("jwt-abc".to_string()) })
126            }
127        }
128        let a = Auth::provider(Arc::new(P));
129        let dbg = format!("{a:?}");
130        assert!(dbg.contains("Provider"));
131        assert!(!dbg.contains("jwt-abc"));
132    }
133}