1#[derive(Clone, PartialEq, Eq)]
22pub struct SecretString(String);
23
24impl SecretString {
25 #[must_use]
28 pub fn new(value: String) -> Self {
29 Self(value)
30 }
31
32 #[must_use]
35 pub fn expose(&self) -> &str {
36 &self.0
37 }
38
39 #[must_use]
42 pub fn len(&self) -> usize {
43 self.0.len()
44 }
45
46 #[must_use]
48 pub fn is_empty(&self) -> bool {
49 self.0.is_empty()
50 }
51}
52
53impl core::fmt::Debug for SecretString {
54 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55 f.write_str("SecretString(<redacted>)")
56 }
57}
58
59impl core::fmt::Display for SecretString {
60 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
61 f.write_str("<redacted>")
62 }
63}
64
65#[cfg(feature = "serde")]
67impl serde::Serialize for SecretString {
68 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
69 serializer.serialize_str("<redacted>")
70 }
71}
72
73macro_rules! secret_newtype {
76 ($(#[$meta:meta])* $name:ident) => {
77 $(#[$meta])*
78 #[derive(Clone, PartialEq, Eq, Debug)]
79 pub struct $name(SecretString);
80
81 impl $name {
82 #[must_use]
84 pub fn new(value: String) -> Self {
85 Self(SecretString::new(value))
86 }
87
88 #[must_use]
90 pub fn expose(&self) -> &str {
91 self.0.expose()
92 }
93
94 #[must_use]
96 pub fn as_secret(&self) -> &SecretString {
97 &self.0
98 }
99 }
100
101 #[cfg(feature = "serde")]
102 impl serde::Serialize for $name {
103 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
104 self.0.serialize(s)
105 }
106 }
107 };
108}
109
110secret_newtype! {
111 PlainCode
114}
115
116secret_newtype! {
117 SessionSecret
120}
121
122secret_newtype! {
123 FormTokenSecret
126}
127
128macro_rules! id_newtype {
130 ($(#[$meta:meta])* $name:ident) => {
131 $(#[$meta])*
132 #[derive(Clone, PartialEq, Eq, Hash, Debug)]
133 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134 pub struct $name(String);
135
136 impl $name {
137 #[must_use]
139 pub fn new(value: String) -> Self {
140 Self(value)
141 }
142
143 #[must_use]
145 pub fn as_str(&self) -> &str {
146 &self.0
147 }
148 }
149
150 impl core::fmt::Display for $name {
151 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
152 f.write_str(&self.0)
153 }
154 }
155
156 impl From<String> for $name {
157 fn from(value: String) -> Self {
158 Self(value)
159 }
160 }
161 };
162}
163
164id_newtype! {
165 CodeId
167}
168
169id_newtype! {
170 SubjectId
173}
174
175id_newtype! {
176 SessionId
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 #[test]
185 fn secret_string_redacts_debug_and_display() {
186 let s = SecretString::new("hunter2".to_string());
187 assert_eq!(format!("{s:?}"), "SecretString(<redacted>)");
188 assert_eq!(format!("{s}"), "<redacted>");
189 assert!(!format!("{s:?}").contains("hunter2"));
191 assert!(!format!("{s}").contains("hunter2"));
192 assert_eq!(s.expose(), "hunter2");
194 }
195
196 #[test]
197 fn secret_newtypes_redact_debug() {
198 let c = PlainCode::new("ABCD2345".to_string());
199 let dbg = format!("{c:?}");
200 assert!(
201 !dbg.contains("ABCD2345"),
202 "PlainCode Debug leaked plaintext: {dbg}"
203 );
204 assert!(dbg.contains("<redacted>"));
205 assert_eq!(c.expose(), "ABCD2345");
206 }
207
208 #[test]
209 fn id_newtype_displays_and_roundtrips() {
210 let id = CodeId::new("abc123".to_string());
211 assert_eq!(id.as_str(), "abc123");
212 assert_eq!(format!("{id}"), "abc123");
213 assert_eq!(CodeId::from("x".to_string()).as_str(), "x");
214 }
215
216 #[cfg(feature = "serde")]
217 #[test]
218 fn secret_serializes_redacted() {
219 let c = SessionSecret::new("supersecret".to_string());
220 let json = serde_json::to_string(&c).unwrap();
221 assert_eq!(json, "\"<redacted>\"");
222 assert!(!json.contains("supersecret"));
223 }
224}
225
226#[derive(Clone, PartialEq, Eq, Debug)]
234pub struct NormalizedCode(String);
235
236impl NormalizedCode {
237 #[must_use]
240 pub fn new(value: String) -> Self {
241 Self(value)
242 }
243
244 #[must_use]
246 pub fn as_str(&self) -> &str {
247 &self.0
248 }
249}
250
251impl core::fmt::Display for NormalizedCode {
252 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
253 f.write_str(&self.0)
255 }
256}
257
258#[derive(Clone, PartialEq, Eq, Hash, Debug)]
262#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
263pub struct Purpose(String);
264
265impl Purpose {
266 #[must_use]
268 pub fn new(value: impl Into<String>) -> Option<Self> {
269 let s: String = value.into();
270 if s.is_empty() { None } else { Some(Self(s)) }
271 }
272
273 #[must_use]
275 pub fn as_str(&self) -> &str {
276 &self.0
277 }
278}
279
280impl core::fmt::Display for Purpose {
281 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
282 f.write_str(&self.0)
283 }
284}
285
286#[derive(Clone, PartialEq, Eq, Hash, Debug)]
291#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
292pub struct ScopeKey(String);
293
294impl ScopeKey {
295 #[must_use]
297 pub fn new(value: impl Into<String>) -> Self {
298 Self(value.into())
299 }
300
301 #[must_use]
303 pub fn as_str(&self) -> &str {
304 &self.0
305 }
306}
307
308impl core::fmt::Display for ScopeKey {
309 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
310 f.write_str(&self.0)
311 }
312}
313
314#[cfg(test)]
315mod rfc019_tests {
316 use super::*;
317
318 #[test]
319 fn normalized_code_displays_plainly() {
320 let n = NormalizedCode::new("ABCD2345".into());
321 assert_eq!(format!("{n}"), "ABCD2345");
322 assert_eq!(n.as_str(), "ABCD2345");
323 }
324
325 #[test]
326 fn purpose_rejects_empty() {
327 assert!(Purpose::new("").is_none());
328 assert!(Purpose::new("logout").is_some());
329 }
330
331 #[test]
332 fn scope_key_roundtrip() {
333 let s = ScopeKey::new("community-42");
334 assert_eq!(s.as_str(), "community-42");
335 assert_eq!(format!("{s}"), "community-42");
336 }
337}