1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
use thiserror::Error;
/// Errors that can occur during [`crate::CliTokenClient::run_authorization_flow`].
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum AuthError {
/// The loopback callback server failed to bind.
#[error("failed to bind loopback server: {0}")]
ServerBind(#[source] std::io::Error),
/// The system browser could not be opened.
#[error("failed to open browser: {0}")]
Browser(String),
/// A URL could not be parsed.
#[error("invalid URL: {0}")]
InvalidUrl(#[from] url::ParseError),
/// The callback was not received within the configured timeout.
#[error("authentication timed out")]
Timeout,
/// The user cancelled the flow (Ctrl+C).
#[error("authentication cancelled")]
Cancelled,
/// The token endpoint returned a non-2xx response.
#[error("token exchange failed (HTTP {status}): {body}")]
TokenExchange {
/// HTTP status code returned by the token endpoint.
status: u16,
/// Response body from the token endpoint.
body: String,
},
/// A network-level request error occurred.
#[error("request failed: {0}")]
Request(#[from] reqwest::Error),
/// The token endpoint returned a 2xx response whose body could not be
/// parsed into the expected token response structure.
#[error("failed to parse token response: {0}")]
TokenParse(String),
/// An internal server or channel error occurred.
#[error("server error: {0}")]
Server(String),
/// A required query parameter was absent from the callback request.
#[error("missing callback parameter: {0}")]
MissingCallbackParam(String),
/// An error occurred during callback validation (state mismatch or provider error).
#[error(transparent)]
Callback(#[from] CallbackError),
/// An error occurred while validating the `id_token`.
#[error(transparent)]
IdToken(#[from] IdTokenError),
}
/// Errors that can occur during OAuth 2.0 callback validation.
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub enum CallbackError {
/// The `state` parameter in the callback did not match - possible CSRF attack.
#[error("state parameter mismatch: possible CSRF attack")]
StateMismatch,
/// The authorization provider returned an error in the callback.
#[error("provider error: {error}: {description}")]
ProviderError {
/// The OAuth 2.0 error code (e.g. `access_denied`).
error: String,
/// Human-readable description from the provider.
description: String,
},
}
/// Errors that can occur during [`crate::CliTokenClient::refresh`] or
/// [`crate::CliTokenClient::refresh_if_expiring`].
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum RefreshError {
/// No refresh token is available to exchange.
#[error("no refresh token available")]
NoRefreshToken,
/// The token endpoint returned a non-2xx response.
#[error("token exchange failed (HTTP {status}): {body}")]
TokenExchange {
/// HTTP status code returned by the token endpoint.
status: u16,
/// Response body from the token endpoint.
body: String,
},
/// A network-level request error occurred.
#[error("request failed: {0}")]
Request(#[from] reqwest::Error),
/// The token endpoint returned a 2xx response whose body could not be
/// parsed into the expected token response structure.
#[error("failed to parse token response: {0}")]
TokenParse(String),
/// An error occurred while validating the `id_token`.
#[error(transparent)]
IdToken(#[from] IdTokenError),
}
/// Errors that can occur while validating an `id_token` after a successful token exchange.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum IdTokenError {
/// The JWKS validator rejected the `id_token`.
#[error("JWKS validation failed: {0}")]
JwksValidationFailed(#[source] crate::JwksValidationError),
/// The `openid` scope was requested but the provider did not return an `id_token`.
#[error("openid scope was requested but no id_token was returned")]
NoIdToken,
/// The `id_token` could not be parsed (malformed JWT, missing required claims).
#[error("malformed id_token: {0}")]
MalformedIdToken(String),
/// The `id_token` has expired (`exp` claim is in the past).
#[error("id_token has expired")]
Expired,
/// The `id_token` is not yet valid (`nbf` claim is in the future).
#[error("id_token is not yet valid")]
NotYetValid,
/// The `aud` claim does not include the configured `client_id`.
#[error("id_token audience does not include client_id")]
InvalidAudience,
/// The `iss` claim does not match the configured issuer.
#[error("id_token issuer mismatch: expected {expected}, got {got}")]
InvalidIssuer {
/// The issuer that was expected (from configuration).
expected: String,
/// The issuer found in the `id_token`.
got: String,
},
/// The `nonce` claim is absent or does not match the value sent in the authorization request.
#[error("id_token nonce mismatch")]
NonceMismatch,
}
/// Errors that can occur in [`crate::TokenStore`] implementations.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum TokenStoreError {
/// An I/O error occurred while reading or writing token storage.
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
/// Token data could not be serialized or deserialized.
#[error("serialization error: {0}")]
Serialization(String),
}
#[cfg(test)]
mod tests {
use super::{AuthError, CallbackError, RefreshError, TokenStoreError};
#[test]
fn auth_error_state_mismatch_message() {
assert_eq!(
AuthError::Callback(CallbackError::StateMismatch).to_string(),
"state parameter mismatch: possible CSRF attack"
);
}
#[test]
fn auth_error_timeout_message() {
assert_eq!(AuthError::Timeout.to_string(), "authentication timed out");
}
#[test]
fn auth_error_cancelled_message() {
assert_eq!(AuthError::Cancelled.to_string(), "authentication cancelled");
}
#[test]
fn auth_error_token_exchange_contains_status() {
let err = AuthError::TokenExchange {
status: 401,
body: "Unauthorized".to_string(),
};
assert!(err.to_string().contains("401"));
}
#[test]
fn refresh_error_no_refresh_token_message() {
assert_eq!(
RefreshError::NoRefreshToken.to_string(),
"no refresh token available"
);
}
#[test]
fn auth_error_token_parse_contains_message() {
let err = AuthError::TokenParse("bad json".to_string());
assert!(err.to_string().contains("bad json"));
}
#[test]
fn refresh_error_token_parse_contains_message() {
let err = RefreshError::TokenParse("bad json".to_string());
assert!(err.to_string().contains("bad json"));
}
#[test]
fn token_store_error_serialization_contains_message() {
let err = TokenStoreError::Serialization("bad json".to_string());
assert!(err.to_string().contains("bad json"));
}
}