Skip to main content

aster_forge_auth/
ids.rs

1//! Validated identifiers and injectable refresh ID generation.
2
3use 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            /// Constructs a nonempty identifier of at most 256 bytes without control characters.
15            ///
16            /// # Errors
17            /// Returns `InvalidPolicy` for invalid identifiers. Deserialization applies the same
18            /// checks, so signed JSON cannot bypass the constructor's invariants.
19            pub fn new(value: impl Into<String>) -> Result<Self> {
20                let value = value.into();
21                validate_identifier(&value)?;
22                Ok(Self(value))
23            }
24            /// Returns the storage/wire identity; never log refresh JTIs.
25            #[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
67/// Supplies unpredictable refresh IDs in production and deterministic IDs in tests.
68pub trait IdSource: Send + Sync {
69    /// Generates the next refresh identifier.
70    ///
71    /// # Errors
72    /// Returns a mechanism error when generation fails. Production sources must supply unique,
73    /// unpredictable values; stores must also enforce JTI uniqueness.
74    fn next_id(&self) -> Result<RefreshJti>;
75}
76
77/// Production ID source using the upstream UUID v4 implementation.
78#[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}