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 (lookup_key, _) = self
121 .hasher
122 .lookup_key(SecretDomain::FormToken, raw_token)
123 .map_err(FormTokenError::from_key)?;
124
125 let now = self.clock.unix_now();
126 let (outcome, result_ref) = self
127 .store
128 .consume_form_token(&lookup_key, subject, purpose, bound_resource, now)
129 .await
130 .map_err(FormTokenError::from_store)?;
131
132 match outcome {
133 TokenConsumeOutcome::Proceed => Ok(None),
134 TokenConsumeOutcome::Replay => {
135 self.audit.record(CodeAuthEvent::FormTokenReplay {
136 purpose: purpose.to_string(),
137 });
138 Ok(result_ref)
139 }
140 TokenConsumeOutcome::Invalid => Err(FormTokenError::Invalid {
141 public: crate::error::PublicFormError::ExpiredOrInvalid,
142 }),
143 }
144 }
145
146 pub async fn set_result(
151 &self,
152 raw_token: &str,
153 result_ref: &str,
154 ) -> Result<(), FormTokenError> {
155 let (lookup_key, _) = self
156 .hasher
157 .lookup_key(SecretDomain::FormToken, raw_token)
158 .map_err(FormTokenError::from_key)?;
159 self.store
160 .set_token_result(&lookup_key, result_ref)
161 .await
162 .map_err(FormTokenError::from_store)
163 }
164}
165
166fn hex_lower(bytes: &[u8]) -> String {
167 const HEX: &[u8; 16] = b"0123456789abcdef";
168 let mut s = String::with_capacity(bytes.len() * 2);
169 for &b in bytes {
170 s.push(HEX[(b >> 4) as usize] as char);
171 s.push(HEX[(b & 0xf) as usize] as char);
172 }
173 s
174}