Skip to content

Commit cd3b54c

Browse files
committed
Handle error responses from the OAuth 2.0 provider better
1 parent fdf3881 commit cd3b54c

File tree

6 files changed

+107
-16
lines changed

6 files changed

+107
-16
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/axum-utils/src/client_authorization.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ async fn fetch_jwks(
190190
.get(uri.as_str())
191191
.send_traced()
192192
.await?
193+
.error_for_status()?
193194
.json()
194195
.await?;
195196

crates/oidc-client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ repository.workspace = true
1212
workspace = true
1313

1414
[dependencies]
15+
async-trait.workspace = true
1516
base64ct = { version = "1.6.0", features = ["std"] }
1617
chrono.workspace = true
1718
form_urlencoded = "1.2.1"

crates/oidc-client/src/error.rs

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
//! The error types used in this crate.
88
9+
use async_trait::async_trait;
910
use mas_jose::{
1011
claims::ClaimError,
1112
jwa::InvalidAlgorithm,
1213
jwt::{JwtDecodeError, JwtSignatureError, NoKeyWorked},
1314
};
1415
use oauth2_types::{oidc::ProviderMetadataVerificationError, pkce::CodeChallengeError};
16+
use serde::Deserialize;
1517
use thiserror::Error;
1618

1719
/// All possible errors when using this crate.
@@ -42,17 +44,15 @@ pub enum Error {
4244

4345
/// All possible errors when fetching provider metadata.
4446
#[derive(Debug, Error)]
47+
#[error("Fetching provider metadata failed")]
4548
pub enum DiscoveryError {
4649
/// An error occurred building the request's URL.
47-
#[error(transparent)]
4850
IntoUrl(#[from] url::ParseError),
4951

5052
/// The server returned an HTTP error status code.
51-
#[error(transparent)]
5253
Http(#[from] reqwest::Error),
5354

5455
/// An error occurred validating the metadata.
55-
#[error(transparent)]
5656
Validation(#[from] ProviderMetadataVerificationError),
5757

5858
/// Discovery is disabled for this provider.
@@ -62,25 +62,26 @@ pub enum DiscoveryError {
6262

6363
/// All possible errors when authorizing the client.
6464
#[derive(Debug, Error)]
65+
#[error("Building the authorization URL failed")]
6566
pub enum AuthorizationError {
6667
/// An error occurred constructing the PKCE code challenge.
67-
#[error(transparent)]
6868
Pkce(#[from] CodeChallengeError),
6969

7070
/// An error occurred serializing the request.
71-
#[error(transparent)]
7271
UrlEncoded(#[from] serde_urlencoded::ser::Error),
7372
}
7473

7574
/// All possible errors when requesting an access token.
7675
#[derive(Debug, Error)]
76+
#[error("Request to the token endpoint failed")]
7777
pub enum TokenRequestError {
7878
/// The HTTP client returned an error.
79-
#[error(transparent)]
8079
Http(#[from] reqwest::Error),
8180

81+
/// The server returned an error
82+
OAuth2(#[from] OAuth2Error),
83+
8284
/// Error while injecting the client credentials into the request.
83-
#[error(transparent)]
8485
Credentials(#[from] CredentialsError),
8586
}
8687

@@ -92,7 +93,7 @@ pub enum TokenAuthorizationCodeError {
9293
Token(#[from] TokenRequestError),
9394

9495
/// An error occurred validating the ID Token.
95-
#[error(transparent)]
96+
#[error("Verifying the 'id_token' returned by the provider failed")]
9697
IdToken(#[from] IdTokenError),
9798
}
9899

@@ -104,7 +105,7 @@ pub enum TokenRefreshError {
104105
Token(#[from] TokenRequestError),
105106

106107
/// An error occurred validating the ID Token.
107-
#[error(transparent)]
108+
#[error("Verifying the 'id_token' returned by the provider failed")]
108109
IdToken(#[from] IdTokenError),
109110
}
110111

@@ -129,12 +130,16 @@ pub enum UserInfoError {
129130
},
130131

131132
/// An error occurred verifying the Id Token.
132-
#[error(transparent)]
133+
#[error("Verifying the 'id_token' returned by the provider failed")]
133134
IdToken(#[from] IdTokenError),
134135

135136
/// An error occurred sending the request.
136137
#[error(transparent)]
137138
Http(#[from] reqwest::Error),
139+
140+
/// The server returned an error
141+
#[error(transparent)]
142+
OAuth2(#[from] OAuth2Error),
138143
}
139144

140145
/// All possible errors when requesting a JWKS.
@@ -178,12 +183,12 @@ pub enum IdTokenError {
178183
#[error("Authorization ID token is missing")]
179184
MissingAuthIdToken,
180185

181-
/// An error occurred validating the ID Token's signature and basic claims.
182186
#[error(transparent)]
187+
/// An error occurred validating the ID Token's signature and basic claims.
183188
Jwt(#[from] JwtVerificationError),
184189

185-
/// An error occurred extracting a claim.
186190
#[error(transparent)]
191+
/// An error occurred extracting a claim.
187192
Claim(#[from] ClaimError),
188193

189194
/// The subject identifier returned by the issuer is not the same as the one
@@ -225,3 +230,78 @@ pub enum CredentialsError {
225230
#[error(transparent)]
226231
JwtSignature(#[from] JwtSignatureError),
227232
}
233+
234+
#[derive(Debug, Deserialize)]
235+
struct OAuth2ErrorResponse {
236+
error: String,
237+
error_description: Option<String>,
238+
error_uri: Option<String>,
239+
}
240+
241+
impl std::fmt::Display for OAuth2ErrorResponse {
242+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243+
write!(f, "{:?}", self.error)?;
244+
245+
if let Some(error_uri) = &self.error_uri {
246+
write!(f, " (See {error_uri})")?;
247+
}
248+
249+
if let Some(error_description) = &self.error_description {
250+
write!(f, ": {error_description}")?;
251+
}
252+
253+
Ok(())
254+
}
255+
}
256+
257+
/// An error returned by the OAuth 2.0 provider
258+
#[derive(Debug, Error)]
259+
pub struct OAuth2Error {
260+
error: Option<OAuth2ErrorResponse>,
261+
262+
#[source]
263+
inner: reqwest::Error,
264+
}
265+
266+
impl std::fmt::Display for OAuth2Error {
267+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268+
if let Some(error) = &self.error {
269+
write!(
270+
f,
271+
"Request to the provider failed with the following error: {error}"
272+
)
273+
} else {
274+
write!(f, "Request to the provider failed")
275+
}
276+
}
277+
}
278+
279+
impl From<reqwest::Error> for OAuth2Error {
280+
fn from(inner: reqwest::Error) -> Self {
281+
Self { error: None, inner }
282+
}
283+
}
284+
285+
/// An extension trait to deal with error responses from the OAuth 2.0 provider
286+
#[async_trait]
287+
pub(crate) trait ResponseExt {
288+
async fn error_from_oauth2_error_response(self) -> Result<Self, OAuth2Error>
289+
where
290+
Self: Sized;
291+
}
292+
293+
#[async_trait]
294+
impl ResponseExt for reqwest::Response {
295+
async fn error_from_oauth2_error_response(self) -> Result<Self, OAuth2Error> {
296+
let Err(inner) = self.error_for_status_ref() else {
297+
return Ok(self);
298+
};
299+
300+
let error: OAuth2ErrorResponse = self.json().await?;
301+
302+
Err(OAuth2Error {
303+
error: Some(error),
304+
inner,
305+
})
306+
}
307+
}

crates/oidc-client/src/requests/token.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ use oauth2_types::requests::{AccessTokenRequest, AccessTokenResponse};
1212
use rand::Rng;
1313
use url::Url;
1414

15-
use crate::{error::TokenRequestError, types::client_credentials::ClientCredentials};
15+
use crate::{
16+
error::{ResponseExt, TokenRequestError},
17+
types::client_credentials::ClientCredentials,
18+
};
1619

1720
/// Request an access token.
1821
///
@@ -51,7 +54,8 @@ pub async fn request_access_token(
5154
.authenticated_form(token_request, &request, now, rng)?
5255
.send_traced()
5356
.await?
54-
.error_for_status()?
57+
.error_from_oauth2_error_response()
58+
.await?
5559
.json()
5660
.await?;
5761

crates/oidc-client/src/requests/userinfo.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use url::Url;
2020

2121
use super::jose::JwtVerificationData;
2222
use crate::{
23-
error::{IdTokenError, UserInfoError},
23+
error::{IdTokenError, ResponseExt, UserInfoError},
2424
requests::jose::verify_signed_jwt,
2525
types::IdToken,
2626
};
@@ -74,7 +74,11 @@ pub async fn fetch_userinfo(
7474
.bearer_auth(access_token)
7575
.header(ACCEPT, HeaderValue::from_static(expected_content_type));
7676

77-
let userinfo_response = userinfo_request.send_traced().await?.error_for_status()?;
77+
let userinfo_response = userinfo_request
78+
.send_traced()
79+
.await?
80+
.error_from_oauth2_error_response()
81+
.await?;
7882

7983
let content_type: Mime = userinfo_response
8084
.headers()

0 commit comments

Comments
 (0)