1pub mod jwt;
15
16pub mod nonce;
18
19#[cfg(feature = "es256")]
21pub mod es256;
22
23#[cfg(feature = "eddsa")]
25pub mod eddsa;
26
27use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
28use serde::{Deserialize, Serialize};
29use serde_json::Value;
30use sha2::{Digest, Sha256};
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub enum ResponseType {
35 #[serde(rename = "id_token")]
37 IdToken,
38 #[serde(rename = "vp_token")]
40 VpToken,
41 #[serde(rename = "vp_token id_token")]
43 VpTokenIdToken,
44 #[serde(rename = "code")]
46 Code,
47}
48
49impl std::fmt::Display for ResponseType {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 Self::IdToken => write!(f, "id_token"),
53 Self::VpToken => write!(f, "vp_token"),
54 Self::VpTokenIdToken => write!(f, "vp_token id_token"),
55 Self::Code => write!(f, "code"),
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub enum ResponseMode {
63 #[serde(rename = "fragment")]
65 Fragment,
66 #[serde(rename = "direct_post")]
68 DirectPost,
69 #[serde(rename = "direct_post.jwt")]
71 DirectPostJwt,
72}
73
74impl std::fmt::Display for ResponseMode {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 Self::Fragment => write!(f, "fragment"),
78 Self::DirectPost => write!(f, "direct_post"),
79 Self::DirectPostJwt => write!(f, "direct_post.jwt"),
80 }
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(untagged)]
89pub enum SubjectSyntaxType {
90 JwkThumbprint,
93 Did(String),
95}
96
97impl SubjectSyntaxType {
98 pub const JWK_THUMBPRINT_URN: &'static str = "urn:ietf:params:oauth:jwk-thumbprint";
100
101 pub fn parse(s: &str) -> Self {
103 if s == Self::JWK_THUMBPRINT_URN {
104 Self::JwkThumbprint
105 } else {
106 Self::Did(s.to_string())
107 }
108 }
109
110 pub fn as_str(&self) -> &str {
112 match self {
113 Self::JwkThumbprint => Self::JWK_THUMBPRINT_URN,
114 Self::Did(method) => method,
115 }
116 }
117
118 pub fn is_did(&self) -> bool {
120 matches!(self, Self::Did(_))
121 }
122}
123
124impl std::fmt::Display for SubjectSyntaxType {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 write!(f, "{}", self.as_str())
127 }
128}
129
130pub fn compute_jwk_thumbprint(jwk: &Value) -> Option<String> {
141 let kty = jwk.get("kty")?.as_str()?;
142
143 let canonical = match kty {
144 "EC" => {
145 let crv = jwk.get("crv")?.as_str()?;
146 let x = jwk.get("x")?.as_str()?;
147 let y = jwk.get("y")?.as_str()?;
148 format!(r#"{{"crv":"{crv}","kty":"EC","x":"{x}","y":"{y}"}}"#)
149 }
150 "OKP" => {
151 let crv = jwk.get("crv")?.as_str()?;
152 let x = jwk.get("x")?.as_str()?;
153 format!(r#"{{"crv":"{crv}","kty":"OKP","x":"{x}"}}"#)
154 }
155 "RSA" => {
156 let e = jwk.get("e")?.as_str()?;
157 let n = jwk.get("n")?.as_str()?;
158 format!(r#"{{"e":"{e}","kty":"RSA","n":"{n}"}}"#)
159 }
160 _ => return None,
161 };
162
163 let hash = Sha256::digest(canonical.as_bytes());
164 Some(URL_SAFE_NO_PAD.encode(hash))
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct DisplayProperties {
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub name: Option<String>,
173 #[serde(skip_serializing_if = "Option::is_none")]
175 pub locale: Option<String>,
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub logo: Option<LogoProperties>,
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub background_color: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub text_color: Option<String>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct LogoProperties {
190 pub uri: String,
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub alt_text: Option<String>,
195}
196
197#[derive(Debug, Clone, Default, Serialize, Deserialize)]
199pub struct ClientMetadata {
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub subject_syntax_types_supported: Option<Vec<String>>,
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub id_token_signed_response_alg: Option<String>,
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub redirect_uris: Option<Vec<String>>,
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub policy_uri: Option<String>,
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub tos_uri: Option<String>,
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub logo_uri: Option<String>,
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub client_name: Option<String>,
221 #[serde(flatten)]
223 pub additional: serde_json::Map<String, Value>,
224}
225
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
228#[non_exhaustive]
229pub enum OAuthError {
230 #[error("invalid_request")]
231 #[serde(rename = "invalid_request")]
232 InvalidRequest,
233 #[error("unauthorized_client")]
234 #[serde(rename = "unauthorized_client")]
235 UnauthorizedClient,
236 #[error("access_denied")]
237 #[serde(rename = "access_denied")]
238 AccessDenied,
239 #[error("unsupported_response_type")]
240 #[serde(rename = "unsupported_response_type")]
241 UnsupportedResponseType,
242 #[error("invalid_scope")]
243 #[serde(rename = "invalid_scope")]
244 InvalidScope,
245 #[error("server_error")]
246 #[serde(rename = "server_error")]
247 ServerError,
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use serde_json::json;
254
255 #[test]
256 fn jwk_thumbprint_ec_p256() {
257 let jwk = json!({
259 "kty": "EC",
260 "crv": "P-256",
261 "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc",
262 "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ"
263 });
264
265 let thumbprint = compute_jwk_thumbprint(&jwk).unwrap();
266 assert!(!thumbprint.is_empty());
267 assert_eq!(thumbprint.len(), 43);
269 }
270
271 #[test]
272 fn jwk_thumbprint_okp_ed25519() {
273 let jwk = json!({
274 "kty": "OKP",
275 "crv": "Ed25519",
276 "x": "Xx4_L89E6RsyvDTzN9wuN3cDwgifPkXMgFJv_HMIxdk"
277 });
278
279 let thumbprint = compute_jwk_thumbprint(&jwk).unwrap();
280 assert_eq!(thumbprint.len(), 43);
281 }
282
283 #[test]
284 fn jwk_thumbprint_deterministic() {
285 let jwk = json!({"kty": "EC", "crv": "P-256", "x": "abc", "y": "def"});
286 let t1 = compute_jwk_thumbprint(&jwk).unwrap();
287 let t2 = compute_jwk_thumbprint(&jwk).unwrap();
288 assert_eq!(t1, t2);
289 }
290
291 #[test]
292 fn jwk_thumbprint_different_keys() {
293 let jwk1 = json!({"kty": "EC", "crv": "P-256", "x": "a", "y": "b"});
294 let jwk2 = json!({"kty": "EC", "crv": "P-256", "x": "c", "y": "d"});
295 assert_ne!(compute_jwk_thumbprint(&jwk1), compute_jwk_thumbprint(&jwk2));
296 }
297
298 #[test]
299 fn jwk_thumbprint_ignores_extra_fields() {
300 let jwk1 = json!({"kty": "EC", "crv": "P-256", "x": "a", "y": "b"});
301 let jwk2 = json!({"kty": "EC", "crv": "P-256", "x": "a", "y": "b", "kid": "extra"});
302 assert_eq!(compute_jwk_thumbprint(&jwk1), compute_jwk_thumbprint(&jwk2));
303 }
304
305 #[test]
306 fn jwk_thumbprint_unsupported_kty() {
307 let jwk = json!({"kty": "oct", "k": "secret"});
308 assert!(compute_jwk_thumbprint(&jwk).is_none());
309 }
310
311 #[test]
312 fn response_type_display() {
313 assert_eq!(ResponseType::IdToken.to_string(), "id_token");
314 assert_eq!(ResponseType::VpToken.to_string(), "vp_token");
315 assert_eq!(
316 ResponseType::VpTokenIdToken.to_string(),
317 "vp_token id_token"
318 );
319 }
320
321 #[test]
322 fn response_mode_display() {
323 assert_eq!(ResponseMode::Fragment.to_string(), "fragment");
324 assert_eq!(ResponseMode::DirectPost.to_string(), "direct_post");
325 assert_eq!(ResponseMode::DirectPostJwt.to_string(), "direct_post.jwt");
326 }
327
328 #[test]
329 fn subject_syntax_type_parsing() {
330 let jwk = SubjectSyntaxType::parse("urn:ietf:params:oauth:jwk-thumbprint");
331 assert_eq!(jwk, SubjectSyntaxType::JwkThumbprint);
332 assert!(!jwk.is_did());
333
334 let did = SubjectSyntaxType::parse("did:key");
335 assert!(did.is_did());
336 assert_eq!(did.as_str(), "did:key");
337 }
338
339 #[test]
340 fn client_metadata_serialization() {
341 let meta = ClientMetadata {
342 client_name: Some("Test RP".into()),
343 subject_syntax_types_supported: Some(vec![
344 "urn:ietf:params:oauth:jwk-thumbprint".into(),
345 "did:key".into(),
346 ]),
347 ..Default::default()
348 };
349
350 let json = serde_json::to_string(&meta).unwrap();
351 assert!(json.contains("Test RP"));
352 assert!(json.contains("did:key"));
353
354 let parsed: ClientMetadata = serde_json::from_str(&json).unwrap();
355 assert_eq!(parsed.client_name.as_deref(), Some("Test RP"));
356 }
357}