Skip to main content

schwab_sdk/
token.rs

1//! Bearer-credential provider for [`SchwabClient`](crate::SchwabClient).
2//!
3//! A trivial implementation is provided:
4//!
5//! - [`StaticTokenProvider`] - returns the same [`AuthToken`] forever.
6//!   This is what [`SchwabClient::new`](crate::SchwabClient::new) wraps
7//!   internally; callers who never need to rotate a token need not
8//!   interact with the trait at all.
9//!
10//! A consumer that wants on-demand refresh, lazy fetch from a secret
11//! store, or any other policy implements [`TokenProvider`] directly.
12//!
13//! # OAuth flow
14//!
15//! The SDK does not perform the authorization-code exchange. Callers
16//! obtain the bearer out of band. If you stand up a local callback
17//! server for the redirect, bind to `127.0.0.1` only, make the
18//! listener one-shot, and validate the `state` parameter on every
19//! callback to prevent CSRF.
20
21use async_trait::async_trait;
22
23use crate::error::Error;
24use crate::secrets::AuthToken;
25
26/// Source of the bearer token used on every Schwab REST request.
27///
28/// The SDK calls [`access_token`](Self::access_token) once per request,
29/// just before sending. A provider that wants to cache should do so
30/// internally; the SDK does not.
31///
32/// The trait itself carries no `Send`/`Sync` bound so `!Send`
33/// implementations remain expressible (tests, future client variants).
34/// The bound is enforced at the storage site: [`SchwabClient`] holds
35/// `Arc<dyn TokenProvider + Send + Sync>`, so a provider handed to
36/// [`SchwabClient::with_token_provider`] must satisfy both.
37///
38/// # Examples
39///
40/// A swappable provider using `arc-swap` for wait-free reads. A refresh
41/// loop calls [`rotate`](#method.rotate) when a new access token arrives
42/// and the next [`access_token`](Self::access_token) call hands it out.
43/// Wire it in with [`SchwabClient::with_token_provider`]. The same provider
44/// is reused across every clone of the client.
45///
46/// ```no_run
47/// use std::sync::Arc;
48/// use arc_swap::ArcSwap;
49/// use async_trait::async_trait;
50/// use schwab_sdk::{AuthToken, Error, SchwabClient, TokenProvider};
51///
52/// struct SwappableProvider(ArcSwap<AuthToken>);
53///
54/// impl SwappableProvider {
55///     fn new(initial: AuthToken) -> Self {
56///         Self(ArcSwap::from_pointee(initial))
57///     }
58///
59///     /// Called by your refresh loop when a fresh access token arrives.
60///     fn rotate(&self, fresh: AuthToken) {
61///         self.0.store(Arc::new(fresh));
62///     }
63/// }
64///
65/// #[async_trait]
66/// impl TokenProvider for SwappableProvider {
67///     async fn access_token(&self) -> Result<AuthToken, Error> {
68///         Ok((*self.0.load_full()).clone())
69///     }
70/// }
71///
72/// async fn run() -> schwab_sdk::Result<()> {
73///     let provider = Arc::new(SwappableProvider::new(AuthToken::new("initial-token")));
74///     let client = SchwabClient::with_token_provider(provider.clone());
75///
76///     // The first REST call sees the initial token.
77///     let _ = client.accounts().numbers().await?;
78///
79///     // Your refresh task obtains a new access token out of band, then
80///     // hands it to the provider.
81///     provider.rotate(AuthToken::new("rotated-token"));
82///
83///     // The next REST call sees the rotated token.
84///     let _ = client.accounts().numbers().await?;
85///
86///     Ok(())
87/// }
88/// ```
89///
90/// [`SchwabClient`]: crate::SchwabClient
91/// [`SchwabClient::with_token_provider`]: crate::SchwabClient::with_token_provider
92#[async_trait]
93pub trait TokenProvider {
94    /// Return the current bearer token. Called once per REST request.
95    ///
96    /// A failure here surfaces as [`Error::TokenProvider`] before any
97    /// network I/O is attempted.
98    async fn access_token(&self) -> Result<AuthToken, Error>;
99}
100
101/// [`TokenProvider`] that returns the same [`AuthToken`] for every call.
102///
103/// This is the default impl wrapping the token passed to
104/// [`SchwabClient::new`](crate::SchwabClient::new); callers who hold a
105/// short-lived token and tear the client down when it expires need no
106/// other provider.
107#[derive(Debug, Clone)]
108pub struct StaticTokenProvider(AuthToken);
109
110impl StaticTokenProvider {
111    /// Wrap an [`AuthToken`] so it can be served as a [`TokenProvider`].
112    pub fn new(token: AuthToken) -> Self {
113        Self(token)
114    }
115}
116
117#[async_trait]
118impl TokenProvider for StaticTokenProvider {
119    async fn access_token(&self) -> Result<AuthToken, Error> {
120        Ok(self.0.clone())
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[tokio::test]
129    async fn static_provider_returns_the_same_token() {
130        let provider = StaticTokenProvider::new(AuthToken::new("abc"));
131        let a = provider.access_token().await.unwrap();
132        let b = provider.access_token().await.unwrap();
133        assert_eq!(a.expose_secret(), "abc");
134        assert_eq!(b.expose_secret(), "abc");
135    }
136
137    #[test]
138    fn static_provider_debug_does_not_leak_token() {
139        let provider = StaticTokenProvider::new(AuthToken::new("super-secret"));
140        let debug = format!("{provider:?}");
141        assert!(
142            !debug.contains("super-secret"),
143            "Debug leaked token: {debug}"
144        );
145    }
146}