1use std::fmt;
2
3use base64::Engine;
4use serde::Deserialize;
5use sha2::Digest;
6
7#[derive(Debug, Clone, Deserialize)]
9pub struct OidcProviderMetadata {
10 pub issuer: String,
12 pub authorization_endpoint: String,
14 pub token_endpoint: String,
16 pub jwks_uri: String,
18 pub userinfo_endpoint: Option<String>,
20 #[serde(default)]
22 pub response_types_supported: Vec<String>,
23 #[serde(default)]
25 pub code_challenge_methods_supported: Vec<String>,
26 #[serde(default)]
28 pub id_token_signing_alg_values_supported: Vec<String>,
29}
30
31#[derive(Clone)]
33pub struct OidcAuthorizationRequest {
34 pub url: String,
36 pub state: String,
38 pub nonce: String,
40 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#[derive(Clone)]
58pub struct OidcTokenExchangeRequest {
59 pub token_endpoint: String,
61 pub code: String,
63 pub redirect_uri: String,
65 pub state: String,
67 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#[derive(Clone)]
89pub struct PkcePair {
90 pub verifier: String,
92 pub challenge: String,
94 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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct OidcClaims {
129 pub sub: String,
131 pub iss: String,
133 pub aud: Vec<String>,
135 pub exp: u64,
137 pub iat: u64,
139 pub nbf: Option<u64>,
141 pub nonce: Option<String>,
143 pub email: Option<String>,
145 pub email_verified: Option<bool>,
147 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#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
167pub struct OidcUserInfo {
168 pub sub: String,
170 pub email: Option<String>,
172 pub email_verified: Option<bool>,
174 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}