Skip to main content

rskit_auth/oidc/
types.rs

1use std::fmt;
2
3use base64::Engine;
4use serde::Deserialize;
5use sha2::Digest;
6
7/// OIDC discovery document.
8#[derive(Debug, Clone, Deserialize)]
9pub struct OidcProviderMetadata {
10    /// Issuer URL.
11    pub issuer: String,
12    /// Authorization endpoint.
13    pub authorization_endpoint: String,
14    /// Token endpoint.
15    pub token_endpoint: String,
16    /// JWKS endpoint.
17    pub jwks_uri: String,
18    /// Userinfo endpoint.
19    pub userinfo_endpoint: Option<String>,
20    /// Supported response types.
21    #[serde(default)]
22    pub response_types_supported: Vec<String>,
23    /// Supported PKCE code challenge methods.
24    #[serde(default)]
25    pub code_challenge_methods_supported: Vec<String>,
26    /// Supported ID-token signing algorithms.
27    #[serde(default)]
28    pub id_token_signing_alg_values_supported: Vec<String>,
29}
30
31/// Built authorization request plus correlated anti-CSRF state.
32#[derive(Clone)]
33pub struct OidcAuthorizationRequest {
34    /// Fully rendered authorization URL.
35    pub url: String,
36    /// Anti-CSRF state value.
37    pub state: String,
38    /// Nonce that must be echoed in the ID token.
39    pub nonce: String,
40    /// PKCE verifier/challenge pair.
41    pub pkce: Option<PkcePair>,
42}
43
44impl fmt::Debug for OidcAuthorizationRequest {
45    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46        formatter
47            .debug_struct("OidcAuthorizationRequest")
48            .field("url", &"<redacted>")
49            .field("state", &"<redacted>")
50            .field("nonce", &"<redacted>")
51            .field("pkce", &self.pkce.as_ref().map(|_| "<redacted>"))
52            .finish()
53    }
54}
55
56/// Token exchange parameters validated against the authorization request state.
57#[derive(Clone)]
58pub struct OidcTokenExchangeRequest {
59    /// Token endpoint discovered from the provider.
60    pub token_endpoint: String,
61    /// Authorization code from the callback.
62    pub code: String,
63    /// Exact redirect URI configured for the client.
64    pub redirect_uri: String,
65    /// Original anti-CSRF state.
66    pub state: String,
67    /// Optional PKCE verifier. Required for public clients.
68    pub code_verifier: Option<String>,
69}
70
71impl fmt::Debug for OidcTokenExchangeRequest {
72    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73        formatter
74            .debug_struct("OidcTokenExchangeRequest")
75            .field("token_endpoint", &self.token_endpoint)
76            .field("code", &"<redacted>")
77            .field("redirect_uri", &self.redirect_uri)
78            .field("state", &"<redacted>")
79            .field(
80                "code_verifier",
81                &self.code_verifier.as_ref().map(|_| "<redacted>"),
82            )
83            .finish()
84    }
85}
86
87/// PKCE verifier/challenge pair.
88#[derive(Clone)]
89pub struct PkcePair {
90    /// High-entropy verifier.
91    pub verifier: String,
92    /// SHA-256 based challenge.
93    pub challenge: String,
94    /// Challenge method, always `S256`.
95    pub method: &'static str,
96}
97
98impl fmt::Debug for PkcePair {
99    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
100        formatter
101            .debug_struct("PkcePair")
102            .field("verifier", &"<redacted>")
103            .field("challenge", &"<redacted>")
104            .field("method", &self.method)
105            .finish()
106    }
107}
108
109impl PkcePair {
110    /// Generate a PKCE verifier/challenge pair.
111    #[must_use]
112    pub fn generate() -> Self {
113        let mut bytes = [0_u8; 32];
114        rand::fill(&mut bytes);
115        let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
116        let challenge = base64::engine::general_purpose::URL_SAFE_NO_PAD
117            .encode(sha2::Sha256::digest(verifier.as_bytes()));
118        Self {
119            verifier,
120            challenge,
121            method: "S256",
122        }
123    }
124}
125
126/// Claims extracted from a validated OIDC ID token.
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct OidcClaims {
129    /// Subject identifier.
130    pub sub: String,
131    /// Issuer identifier.
132    pub iss: String,
133    /// Audience values.
134    pub aud: Vec<String>,
135    /// Expiration timestamp (seconds since epoch).
136    pub exp: u64,
137    /// Issued-at timestamp.
138    pub iat: u64,
139    /// Not-before timestamp, if present.
140    pub nbf: Option<u64>,
141    /// OIDC nonce claim, if present.
142    pub nonce: Option<String>,
143    /// User email, if present.
144    pub email: Option<String>,
145    /// Whether the provider verified the email.
146    pub email_verified: Option<bool>,
147    /// Human-readable display name.
148    pub name: Option<String>,
149}
150
151#[derive(Debug, Clone, Deserialize)]
152pub(super) struct RawOidcClaims {
153    pub(super) sub: String,
154    pub(super) iss: String,
155    pub(super) aud: Audiences,
156    pub(super) exp: u64,
157    pub(super) iat: Option<u64>,
158    pub(super) nbf: Option<u64>,
159    pub(super) nonce: Option<String>,
160    pub(super) email: Option<String>,
161    pub(super) email_verified: Option<bool>,
162    pub(super) name: Option<String>,
163}
164
165/// User profile returned by the OIDC userinfo endpoint.
166#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
167pub struct OidcUserInfo {
168    /// Subject identifier.
169    pub sub: String,
170    /// User email, if present.
171    pub email: Option<String>,
172    /// Whether the provider verified the email.
173    pub email_verified: Option<bool>,
174    /// Human-readable name.
175    pub name: Option<String>,
176}
177
178#[derive(Debug, Clone, Deserialize)]
179#[serde(untagged)]
180pub(super) enum Audiences {
181    One(String),
182    Many(Vec<String>),
183}
184
185impl Audiences {
186    pub(super) fn into_vec(self) -> Vec<String> {
187        match self {
188            Self::One(value) => vec![value],
189            Self::Many(values) => values,
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::{OidcAuthorizationRequest, OidcTokenExchangeRequest, PkcePair};
197
198    #[test]
199    fn oidc_debug_masks_callback_secrets() {
200        let pkce = PkcePair {
201            verifier: "verifier-secret".into(),
202            challenge: "challenge-secret".into(),
203            method: "S256",
204        };
205        let authorization = OidcAuthorizationRequest {
206            url: "https://issuer/authorize?state=state-secret&nonce=nonce-secret".into(),
207            state: "state-secret".into(),
208            nonce: "nonce-secret".into(),
209            pkce: Some(pkce.clone()),
210        };
211        let exchange = OidcTokenExchangeRequest {
212            token_endpoint: "https://issuer/token".into(),
213            code: "code-secret".into(),
214            redirect_uri: "https://app/callback".into(),
215            state: "state-secret".into(),
216            code_verifier: Some(pkce.verifier.clone()),
217        };
218
219        let formatted = format!("{authorization:?} {exchange:?} {pkce:?}");
220
221        for secret in [
222            "state-secret",
223            "nonce-secret",
224            "verifier-secret",
225            "challenge-secret",
226            "code-secret",
227        ] {
228            assert!(!formatted.contains(secret), "{formatted}");
229        }
230    }
231}