1use crate::{AuthError, Result};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7macro_rules! identifier {
8 ($name:ident, $doc:literal, $redact:expr) => {
9 #[doc = $doc]
10 #[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11 #[serde(try_from = "String", into = "String")]
12 pub struct $name(String);
13 impl $name {
14 pub fn new(value: impl Into<String>) -> Result<Self> {
20 let value = value.into();
21 validate_identifier(&value)?;
22 Ok(Self(value))
23 }
24 #[must_use]
26 pub fn as_str(&self) -> &str {
27 &self.0
28 }
29 }
30 impl TryFrom<String> for $name {
31 type Error = AuthError;
32 fn try_from(value: String) -> Result<Self> {
33 Self::new(value)
34 }
35 }
36 impl From<$name> for String {
37 fn from(value: $name) -> Self {
38 value.0
39 }
40 }
41 impl fmt::Debug for $name {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 f.debug_tuple(stringify!($name))
44 .field(&if $redact { "<redacted>" } else { &self.0 })
45 .finish()
46 }
47 }
48 };
49}
50
51identifier!(
52 Subject,
53 "Stable product subject; integer IDs and UUIDs use their string representation.",
54 false
55);
56identifier!(
57 SessionId,
58 "Server-side session identity, distinct from its single-use refresh JTI.",
59 false
60);
61identifier!(
62 RefreshJti,
63 "Single-use refresh identity. Debug output is always redacted.",
64 true
65);
66
67pub trait IdSource: Send + Sync {
69 fn next_id(&self) -> Result<RefreshJti>;
75}
76
77#[derive(Debug, Default, Clone, Copy)]
79pub struct RandomIdSource;
80impl IdSource for RandomIdSource {
81 fn next_id(&self) -> Result<RefreshJti> {
82 RefreshJti::new(uuid::Uuid::new_v4().to_string())
83 }
84}
85
86pub(crate) fn validate_identifier(value: &str) -> Result<()> {
87 if value.trim().is_empty() || value.len() > 256 || value.chars().any(char::is_control) {
88 return Err(AuthError::InvalidPolicy);
89 }
90 Ok(())
91}