codlet_core/auth/
token.rs1use crate::audit::{AuditSink, CodeAuthEvent};
7use crate::clock::Clock;
8use crate::hashing::{KeyProvider, SecretDomain, SecretHasher};
9use crate::rng::RandomSource;
10use crate::secret::FormTokenSecret;
11use crate::state::TokenConsumeOutcome;
12use crate::store::code::expires_at_from_ttl;
13use crate::store::token::{FormTokenRecord, FormTokenStore, TokenSubject};
14
15use super::error::FormTokenError;
16
17pub struct FormTokenManager<TS, K, C, A> {
19 store: TS,
20 hasher: SecretHasher<K>,
21 clock: C,
22 audit: A,
23 ttl: std::time::Duration,
24}
25
26impl<TS, K, C, A> FormTokenManager<TS, K, C, A>
27where
28 TS: FormTokenStore,
29 K: KeyProvider,
30 C: Clock,
31 A: AuditSink,
32{
33 #[must_use]
37 pub fn new(
38 store: TS,
39 hasher: SecretHasher<K>,
40 clock: C,
41 audit: A,
42 ttl: std::time::Duration,
43 ) -> Self {
44 Self {
45 store,
46 hasher,
47 clock,
48 audit,
49 ttl,
50 }
51 }
52
53 pub async fn issue<R: RandomSource>(
62 &self,
63 rng: &mut R,
64 subject: TokenSubject,
65 purpose: impl Into<String>,
66 bound_resource: Option<String>,
67 ) -> Result<FormTokenSecret, FormTokenError> {
68 let mut raw = [0u8; 32];
70 rng.fill_bytes(&mut raw)
71 .map_err(|e| FormTokenError::Internal {
72 cause: format!("rng: {e}"),
73 public: crate::error::PublicFormError::TemporarilyUnavailable,
74 })?;
75 let secret_hex = hex_lower(&raw);
76 let secret = FormTokenSecret::new(secret_hex.clone());
77
78 let (lookup_key, key_version) = self
79 .hasher
80 .lookup_key(SecretDomain::FormToken, secret.expose())
81 .map_err(FormTokenError::from_key)?;
82
83 let now = self.clock.unix_now();
84 let purpose = purpose.into();
85
86 self.store
87 .insert_form_token(FormTokenRecord {
88 lookup_key,
89 key_version,
90 subject,
91 purpose,
92 bound_resource,
93 issued_at: now,
94 expires_at: expires_at_from_ttl(now, self.ttl),
95 })
96 .await
97 .map_err(FormTokenError::from_store)?;
98
99 Ok(secret)
100 }
101
102 pub async fn consume(
114 &self,
115 raw_token: &str,
116 subject: &TokenSubject,
117 purpose: &str,
118 bound_resource: Option<&str>,
119 ) -> Result<Option<String>, FormTokenError> {
120 let candidates: Vec<_> = self
123 .hasher
124 .lookup_key_candidates(SecretDomain::FormToken, raw_token)
125 .map_err(FormTokenError::from_key)?
126 .into_iter()
127 .map(|(lk, _)| lk)
128 .collect();
129
130 let now = self.clock.unix_now();
131 let (outcome, result_ref) = self
132 .store
133 .consume_form_token(&candidates, subject, purpose, bound_resource, now)
134 .await
135 .map_err(FormTokenError::from_store)?;
136
137 match outcome {
138 TokenConsumeOutcome::Proceed => Ok(None),
139 TokenConsumeOutcome::Replay => {
140 self.audit.record(CodeAuthEvent::FormTokenReplay {
141 purpose: purpose.to_string(),
142 });
143 Ok(result_ref)
144 }
145 TokenConsumeOutcome::Invalid => Err(FormTokenError::Invalid {
146 public: crate::error::PublicFormError::ExpiredOrInvalid,
147 }),
148 }
149 }
150
151 pub async fn set_result(
156 &self,
157 raw_token: &str,
158 result_ref: &str,
159 ) -> Result<(), FormTokenError> {
160 let candidates: Vec<_> = self
163 .hasher
164 .lookup_key_candidates(SecretDomain::FormToken, raw_token)
165 .map_err(FormTokenError::from_key)?
166 .into_iter()
167 .map(|(lk, _)| lk)
168 .collect();
169 self.store
170 .set_token_result(&candidates, result_ref)
171 .await
172 .map_err(FormTokenError::from_store)
173 }
174}
175
176fn hex_lower(bytes: &[u8]) -> String {
177 const HEX: &[u8; 16] = b"0123456789abcdef";
178 let mut s = String::with_capacity(bytes.len() * 2);
179 for &b in bytes {
180 s.push(HEX[(b >> 4) as usize] as char);
181 s.push(HEX[(b & 0xf) as usize] as char);
182 }
183 s
184}