1use auths_crypto::error::AuthsErrorInfo;
2use thiserror::Error;
3
4#[derive(Debug, Error)]
15#[non_exhaustive]
16pub enum OidcError {
17 #[error("JWT decode failed: {0}")]
19 JwtDecode(String),
20
21 #[error("signature verification failed")]
23 SignatureVerificationFailed,
24
25 #[error("claim validation failed - {claim}: {reason}")]
27 ClaimsValidationFailed { claim: String, reason: String },
28
29 #[error("unknown key ID: {0}")]
31 UnknownKeyId(String),
32
33 #[error("JWKS resolution failed: {0}")]
35 JwksResolutionFailed(String),
36
37 #[error("algorithm mismatch: expected {expected}, got {got}")]
39 AlgorithmMismatch { expected: String, got: String },
40
41 #[error("token expired (exp: {token_exp}, now: {current_time}, leeway: {leeway}s)")]
43 ClockSkewExceeded {
44 token_exp: i64,
45 current_time: i64,
46 leeway: i64,
47 },
48
49 #[error("token replay detected (jti: {0})")]
51 TokenReplayDetected(String),
52}
53
54impl AuthsErrorInfo for OidcError {
55 fn error_code(&self) -> &'static str {
56 match self {
57 Self::JwtDecode(_) => "AUTHS-E8001",
58 Self::SignatureVerificationFailed => "AUTHS-E8002",
59 Self::ClaimsValidationFailed { .. } => "AUTHS-E8003",
60 Self::UnknownKeyId(_) => "AUTHS-E8004",
61 Self::JwksResolutionFailed(_) => "AUTHS-E8005",
62 Self::AlgorithmMismatch { .. } => "AUTHS-E8006",
63 Self::ClockSkewExceeded { .. } => "AUTHS-E8007",
64 Self::TokenReplayDetected(_) => "AUTHS-E8008",
65 }
66 }
67
68 fn suggestion(&self) -> Option<&'static str> {
69 match self {
70 Self::JwtDecode(_) => {
71 Some("Verify the token format and ensure it is a valid JWT")
72 }
73 Self::SignatureVerificationFailed => {
74 Some("Check that the JWKS endpoint is up-to-date and the token is from a trusted issuer")
75 }
76 Self::ClaimsValidationFailed { claim, .. } => {
77 if claim == "exp" {
78 Some("The token has expired; acquire a new token from the OIDC provider")
79 } else if claim == "iss" {
80 Some("Verify that the token issuer matches the configured trusted issuer")
81 } else if claim == "aud" {
82 Some("Ensure the token audience matches the configured expected audience")
83 } else {
84 Some("Check that the OIDC provider configuration matches the token claims")
85 }
86 }
87 Self::UnknownKeyId(_) => {
88 Some("The JWKS cache may be stale; refresh the JWKS from the issuer endpoint")
89 }
90 Self::JwksResolutionFailed(_) => {
91 Some("Check network connectivity to the JWKS endpoint and ensure the issuer URL is correct")
92 }
93 Self::AlgorithmMismatch { .. } => {
94 Some("Verify that the expected algorithm matches the algorithm used by the OIDC provider")
95 }
96 Self::ClockSkewExceeded { .. } => {
97 Some("Synchronize the system clock or increase the configured clock skew tolerance")
98 }
99 Self::TokenReplayDetected(_) => {
100 Some("A token with this ID has already been used; acquire a new token from the OIDC provider")
101 }
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn test_jwt_decode_error_code() {
112 let err = OidcError::JwtDecode("invalid format".to_string());
113 assert_eq!(err.error_code(), "AUTHS-E8001");
114 assert!(err.suggestion().is_some());
115 }
116
117 #[test]
118 fn test_signature_verification_error_code() {
119 let err = OidcError::SignatureVerificationFailed;
120 assert_eq!(err.error_code(), "AUTHS-E8002");
121 assert!(err.suggestion().is_some());
122 }
123
124 #[test]
125 fn test_claims_validation_error_code() {
126 let err = OidcError::ClaimsValidationFailed {
127 claim: "exp".to_string(),
128 reason: "token expired".to_string(),
129 };
130 assert_eq!(err.error_code(), "AUTHS-E8003");
131 assert!(err.suggestion().is_some());
132 }
133
134 #[test]
135 fn test_unknown_key_id_error_code() {
136 let err = OidcError::UnknownKeyId("key-123".to_string());
137 assert_eq!(err.error_code(), "AUTHS-E8004");
138 assert!(err.suggestion().is_some());
139 }
140
141 #[test]
142 fn test_jwks_resolution_error_code() {
143 let err = OidcError::JwksResolutionFailed("connection timeout".to_string());
144 assert_eq!(err.error_code(), "AUTHS-E8005");
145 assert!(err.suggestion().is_some());
146 }
147
148 #[test]
149 fn test_algorithm_mismatch_error_code() {
150 let err = OidcError::AlgorithmMismatch {
151 expected: "RS256".to_string(),
152 got: "HS256".to_string(),
153 };
154 assert_eq!(err.error_code(), "AUTHS-E8006");
155 assert!(err.suggestion().is_some());
156 }
157
158 #[test]
159 fn test_clock_skew_exceeded_error_code() {
160 let err = OidcError::ClockSkewExceeded {
161 token_exp: 1000,
162 current_time: 2000,
163 leeway: 60,
164 };
165 assert_eq!(err.error_code(), "AUTHS-E8007");
166 assert!(err.suggestion().is_some());
167 }
168
169 #[test]
170 fn test_token_replay_detected_error_code() {
171 let err = OidcError::TokenReplayDetected("jti-123".to_string());
172 assert_eq!(err.error_code(), "AUTHS-E8008");
173 assert!(err.suggestion().is_some());
174 }
175
176 #[test]
177 fn test_all_error_codes_are_unique() {
178 let errors = [
179 OidcError::JwtDecode("".to_string()),
180 OidcError::SignatureVerificationFailed,
181 OidcError::ClaimsValidationFailed {
182 claim: "exp".to_string(),
183 reason: "".to_string(),
184 },
185 OidcError::UnknownKeyId("".to_string()),
186 OidcError::JwksResolutionFailed("".to_string()),
187 OidcError::AlgorithmMismatch {
188 expected: "".to_string(),
189 got: "".to_string(),
190 },
191 OidcError::ClockSkewExceeded {
192 token_exp: 0,
193 current_time: 0,
194 leeway: 0,
195 },
196 OidcError::TokenReplayDetected("".to_string()),
197 ];
198
199 let mut codes: Vec<_> = errors.iter().map(|e| e.error_code()).collect();
200 codes.sort();
201 codes.dedup();
202
203 assert_eq!(codes.len(), errors.len(), "All error codes must be unique");
204 }
205}