1use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
4use std::hash::{Hash, Hasher};
5use std::sync::Arc;
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8use base64::Engine as _;
9use base64::engine::general_purpose::URL_SAFE_NO_PAD;
10use secrecy::{ExposeSecret as _, SecretString};
11use serde::de::DeserializeOwned;
12use serde::{Deserialize, Deserializer, Serialize, Serializer};
13
14use crate::{CredentialError, CredentialResult};
15
16const EXPIRY_MARGIN: Duration = Duration::from_secs(30);
18const REFRESH_MARGIN: Duration = Duration::from_secs(60);
20
21#[derive(Clone)]
23pub struct Token {
24 inner: Arc<TokenInner>,
25}
26
27struct TokenInner {
28 value: SecretString,
29 resource: Option<String>,
32}
33
34impl Token {
35 pub fn expose_secret(&self) -> &str {
37 self.inner.value.expose_secret()
38 }
39
40 pub fn resource(&self) -> Option<&str> {
43 self.inner.resource.as_deref()
44 }
45
46 pub fn with_resource(self, resource: Option<String>) -> Self {
48 Self {
49 inner: Arc::new(TokenInner {
50 value: self.inner.value.clone(),
51 resource,
52 }),
53 }
54 }
55
56 pub fn unverified_claims<T: DeserializeOwned>(&self) -> CredentialResult<T> {
61 let mut segments = self.expose_secret().split('.');
62 let (Some(_header), Some(payload), Some(_signature), None) = (
63 segments.next(),
64 segments.next(),
65 segments.next(),
66 segments.next(),
67 ) else {
68 return Err(CredentialError::invalid("the token is not a JWT"));
69 };
70 let payload = URL_SAFE_NO_PAD
72 .decode(payload.trim_end_matches('='))
73 .map_err(|error| CredentialError::invalid(format!("the JWT payload: {error}")))?;
74 Ok(serde_json::from_slice(&payload)?)
75 }
76
77 pub fn remaining_lifetime(&self) -> CredentialResult<Duration> {
81 let Expiry { exp } = self.unverified_claims()?;
82 let now = SystemTime::now()
83 .duration_since(UNIX_EPOCH)
84 .expect("the clock is after the Unix epoch")
85 .as_secs();
86 Ok(Duration::from_secs(exp.saturating_sub(now)))
87 }
88
89 pub fn is_expired(&self) -> CredentialResult<bool> {
91 Ok(self.remaining_lifetime()? < EXPIRY_MARGIN)
92 }
93
94 pub fn needs_refresh(&self) -> CredentialResult<bool> {
97 Ok(self.remaining_lifetime()? < REFRESH_MARGIN)
98 }
99}
100
101struct Expiry {
103 exp: u64,
104}
105
106impl<'de> Deserialize<'de> for Expiry {
107 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
108 #[derive(Deserialize)]
109 struct Claims {
110 exp: serde_json::Number,
111 }
112 let Claims { exp } = Claims::deserialize(deserializer)?;
113 let exp = exp
114 .as_u64()
115 .or_else(|| {
116 exp.as_f64()
117 .filter(|seconds| seconds.is_finite() && *seconds >= 0.0)
118 .map(|seconds| seconds.round() as u64)
119 })
120 .ok_or_else(|| serde::de::Error::custom("exp must be a non-negative number"))?;
121 Ok(Self { exp })
122 }
123}
124
125impl From<String> for Token {
126 fn from(value: String) -> Self {
127 Self {
128 inner: Arc::new(TokenInner {
129 value: SecretString::from(value),
130 resource: None,
131 }),
132 }
133 }
134}
135
136impl From<&str> for Token {
137 fn from(value: &str) -> Self {
138 Self::from(value.to_owned())
139 }
140}
141
142impl PartialEq for Token {
143 fn eq(&self, other: &Self) -> bool {
144 self.expose_secret() == other.expose_secret()
145 }
146}
147
148impl Eq for Token {}
149
150impl Hash for Token {
151 fn hash<H: Hasher>(&self, state: &mut H) {
152 self.expose_secret().hash(state);
153 }
154}
155
156impl Serialize for Token {
157 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
158 serializer.serialize_str(self.expose_secret())
159 }
160}
161
162impl<'de> Deserialize<'de> for Token {
163 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
164 String::deserialize(deserializer).map(Self::from)
165 }
166}
167
168impl Display for Token {
169 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
170 f.write_str(&"*".repeat(16))
171 }
172}
173
174impl Debug for Token {
175 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
176 f.debug_struct("Token")
177 .field("value", &"*".repeat(16))
178 .finish()
179 }
180}
181
182#[cfg(test)]
184pub(crate) fn unsigned_jwt(claims: serde_json::Value) -> Token {
185 let header = URL_SAFE_NO_PAD.encode(br#"{"alg":"none"}"#);
186 let payload = URL_SAFE_NO_PAD.encode(claims.to_string());
187 Token::from(format!("{header}.{payload}.signature"))
188}
189
190#[cfg(test)]
191mod tests {
192 use serde_json::json;
193
194 use super::*;
195
196 fn now() -> u64 {
197 SystemTime::now()
198 .duration_since(UNIX_EPOCH)
199 .unwrap()
200 .as_secs()
201 }
202
203 #[test]
204 fn remaining_lifetime_tracks_the_exp_claim() {
205 let remaining = unsigned_jwt(json!({ "exp": now() + 600 }))
206 .remaining_lifetime()
207 .unwrap();
208
209 assert!(
211 remaining > Duration::from_secs(598) && remaining <= Duration::from_secs(600),
212 "unexpected remaining lifetime: {remaining:?}"
213 );
214 }
215
216 #[test]
217 fn reads_a_decimal_exp_and_expires_past_tokens() {
218 let token = unsigned_jwt(json!({ "exp": 1_000_000_000.0 }));
219
220 assert_eq!(token.remaining_lifetime().unwrap(), Duration::ZERO);
221 assert!(token.is_expired().unwrap());
222 }
223
224 #[test]
225 fn rejects_a_token_that_is_not_a_jwt() {
226 assert!(Token::from("not-a-jwt").remaining_lifetime().is_err());
227 }
228
229 #[test]
230 fn serializes_as_its_value_and_redacts_debug_output() {
231 let token: Token = serde_json::from_value(json!("very_secret")).unwrap();
232
233 assert_eq!(serde_json::to_value(&token).unwrap(), json!("very_secret"));
234 assert!(!format!("{token:?}").contains("very_secret"));
235 }
236}