reqwest_connect_rpc/token_source.rs
1// Copyright 2025 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! Token source trait for the connect RPC client.
15
16use std::sync::Arc;
17
18use async_trait::async_trait;
19use tokio::sync::watch;
20
21pub mod mock;
22pub mod refresh;
23pub mod static_token;
24
25/// The cause of a [`TokenSourceError`].
26pub type TokenSourceCause = Arc<dyn std::error::Error + Sync + Send>;
27
28/// Why a token source could not provide a token.
29///
30/// A source knows what it authenticates against, so it is the only place that can tell whether
31/// asking again may work. The variant it picks is how it says so; callers read it back through
32/// [`TokenSourceError::is_transient`] instead of interpreting the cause themselves.
33#[derive(Debug, Clone, thiserror::Error)]
34pub enum TokenSourceError {
35 /// No token right now, but a later call may produce one, e.g. because the request to the
36 /// token service did not get through.
37 #[error("token temporarily unavailable: {0}")]
38 Unavailable(TokenSourceCause),
39 /// The credential the source authenticates with was refused, or the answer it got back cannot
40 /// be used as a token. Asking again yields the same answer.
41 #[error("token rejected: {0}")]
42 Rejected(TokenSourceCause),
43 /// The source itself stopped working, e.g. the task that refreshes its token is gone, so it
44 /// will not produce a token again.
45 #[error("token source broken: {0}")]
46 Broken(TokenSourceCause),
47}
48
49impl TokenSourceError {
50 /// The token could not be obtained now, but a later call may succeed.
51 pub fn unavailable(cause: impl Into<Box<dyn std::error::Error + Sync + Send>>) -> Self {
52 Self::Unavailable(cause.into().into())
53 }
54
55 /// The token cannot be obtained, no matter how often it is asked for.
56 pub fn rejected(cause: impl Into<Box<dyn std::error::Error + Sync + Send>>) -> Self {
57 Self::Rejected(cause.into().into())
58 }
59
60 /// The source can no longer hand out tokens at all.
61 pub fn broken(cause: impl Into<Box<dyn std::error::Error + Sync + Send>>) -> Self {
62 Self::Broken(cause.into().into())
63 }
64
65 /// Returns whether the failure is transient, so that a retry may help.
66 ///
67 /// Prefer this over matching the variants: a new variant would silently fall into a caller's
68 /// wildcard arm.
69 #[must_use]
70 pub fn is_transient(&self) -> bool {
71 match self {
72 Self::Unavailable(_) => true,
73 Self::Rejected(_) | Self::Broken(_) => false,
74 }
75 }
76}
77
78/// A watch receiver for token source updates.
79pub type TokenSourceWatch = watch::Receiver<Option<Result<String, TokenSourceError>>>;
80
81/// A source for authentication tokens.
82#[async_trait]
83pub trait TokenSource: Send + Sync + 'static {
84 /// Returns a watch receiver that always holds the latest valid token.
85 ///
86 /// The receiver allows both grabbing the current value immediately
87 /// and awaiting updates.
88 fn watch(&self) -> TokenSourceWatch;
89
90 /// Gets a token, possibly refreshing it.
91 ///
92 /// If the token cannot be obtained, returns the [`TokenSourceError`] the source published,
93 /// classification included, so the caller can tell a source that is momentarily out of reach
94 /// from one that will not produce a token again.
95 ///
96 /// Prefer using `watch` if a subscription to token updates is needed.
97 ///
98 /// ### Implementation Note
99 ///
100 /// The default implementation uses the watch channel to get the latest token.
101 ///``
102 /// - Should be efficient to call multiple times.
103 /// - Errors should be returned if no valid token can be obtained.
104 /// - Should try to not return errors as long as a valid token is available.
105 async fn get_token(&self) -> Result<String, TokenSourceError> {
106 let mut watch = self.watch();
107
108 // First, try to get the current value without waiting. and return immediately if available.
109 // Cloning keeps the published classification; the cause is shared, not copied.
110 match watch.borrow_and_update().as_ref() {
111 Some(Ok(token)) => return Ok(token.clone()),
112 Some(Err(e)) => return Err(e.clone()),
113 None => {}
114 }
115
116 // If there is no current value, wait for an update.
117 watch.changed().await.map_err(TokenSourceError::broken)?;
118
119 // After being notified, get the updated value.
120 match watch.borrow().as_ref() {
121 Some(Ok(token)) => Ok(token.clone()),
122 Some(Err(e)) => Err(e.clone()),
123 None => {
124 Err(TokenSourceError::broken(
125 "token source watch channel has no value",
126 ))
127 }
128 }
129 }
130
131 /// Formats the token for use in an `Authorization` header.
132 ///
133 /// The default implementation formats the token as a Bearer token.
134 /// Override this method if a different format is required.
135 fn format_header(&self, token: String) -> String {
136 format!("Bearer {token}")
137 }
138}