Skip to content

Commit 176fbdd

Browse files
authored
Fail fast if endpoint URLs required by non-core endpoint requests are unusable (#127)
1 parent 6139538 commit 176fbdd

File tree

5 files changed

+83
-62
lines changed

5 files changed

+83
-62
lines changed

examples/google.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ fn main() {
151151

152152
client
153153
.revoke_token(token_to_revoke)
154+
.unwrap()
154155
.request(http_client)
155156
.expect("Failed to revoke token");
156157

examples/google_devicecode.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ fn main() {
6060
// Request the set of codes from the Device Authorization endpoint.
6161
let details: StoringDeviceAuthorizationResponse = device_client
6262
.exchange_device_code()
63+
.unwrap()
6364
.add_scope(Scope::new("profile".to_string()))
6465
.request(http_client)
6566
.expect("Failed to request codes from device auth endpoint");

src/lib.rs

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@
389389
//! .set_device_authorization_url(device_auth_url);
390390
//!
391391
//! let details: StandardDeviceAuthorizationResponse = client
392-
//! .exchange_device_code()
392+
//! .exchange_device_code()?
393393
//! .add_scope(Scope::new("read".to_string()))
394394
//! .request(http_client)?;
395395
//!
@@ -513,6 +513,24 @@ pub use crate::revocation::RevocableToken;
513513
const CONTENT_TYPE_JSON: &str = "application/json";
514514
const CONTENT_TYPE_FORMENCODED: &str = "application/x-www-form-urlencoded";
515515

516+
///
517+
/// There was a problem configuring the request.
518+
///
519+
#[non_exhaustive]
520+
#[derive(Debug, thiserror::Error)]
521+
pub enum ConfigurationError {
522+
///
523+
/// The endpoint URL tp be contacted is missing.
524+
///
525+
#[error("No {0} endpoint URL specified")]
526+
MissingUrl(&'static str),
527+
///
528+
/// The endpoint URL to be contacted MUST be HTTPS.
529+
///
530+
#[error("Scheme for {0} endpoint URL must be HTTPS")]
531+
InsecureUrl(&'static str),
532+
}
533+
516534
///
517535
/// Indicates whether requests to the authorization server should use basic authentication or
518536
/// include the parameters in the request body for requests in which either is valid.
@@ -813,16 +831,21 @@ where
813831
/// Perform a device authorization request as per
814832
/// https://tools.ietf.org/html/rfc8628#section-3.1
815833
///
816-
pub fn exchange_device_code(&self) -> DeviceAuthorizationRequest<TE> {
817-
DeviceAuthorizationRequest {
834+
pub fn exchange_device_code(
835+
&self,
836+
) -> Result<DeviceAuthorizationRequest<TE>, ConfigurationError> {
837+
Ok(DeviceAuthorizationRequest {
818838
auth_type: &self.auth_type,
819839
client_id: &self.client_id,
820840
client_secret: self.client_secret.as_ref(),
821841
extra_params: Vec::new(),
822842
scopes: Vec::new(),
823-
device_authorization_url: self.device_authorization_url.as_ref(),
843+
device_authorization_url: self
844+
.device_authorization_url
845+
.as_ref()
846+
.ok_or(ConfigurationError::MissingUrl("device authorization_url"))?,
824847
_phantom: PhantomData,
825-
}
848+
})
826849
}
827850

828851
///
@@ -856,17 +879,20 @@ where
856879
pub fn introspect<'a>(
857880
&'a self,
858881
token: &'a AccessToken,
859-
) -> IntrospectionRequest<'a, TE, TIR, TT> {
860-
IntrospectionRequest {
882+
) -> Result<IntrospectionRequest<'a, TE, TIR, TT>, ConfigurationError> {
883+
Ok(IntrospectionRequest {
861884
auth_type: &self.auth_type,
862885
client_id: &self.client_id,
863886
client_secret: self.client_secret.as_ref(),
864887
extra_params: Vec::new(),
865-
introspection_url: self.introspection_url.as_ref(),
888+
introspection_url: self
889+
.introspection_url
890+
.as_ref()
891+
.ok_or(ConfigurationError::MissingUrl("introspection"))?,
866892
token,
867893
token_type_hint: None,
868894
_phantom: PhantomData,
869-
}
895+
})
870896
}
871897

872898
///
@@ -878,16 +904,30 @@ where
878904
/// Attempting to submit the generated request without calling [`set_revocation_url()`](Self::set_revocation_url())
879905
/// first will result in an error.
880906
///
881-
pub fn revoke_token(&self, token: RT) -> RevocationRequest<RT, TRE> {
882-
RevocationRequest {
907+
pub fn revoke_token(
908+
&self,
909+
token: RT,
910+
) -> Result<RevocationRequest<RT, TRE>, ConfigurationError> {
911+
// https://tools.ietf.org/html/rfc7009#section-2 states:
912+
// "The client requests the revocation of a particular token by making an
913+
// HTTP POST request to the token revocation endpoint URL. This URL
914+
// MUST conform to the rules given in [RFC6749], Section 3.1. Clients
915+
// MUST verify that the URL is an HTTPS URL."
916+
let revocation_url = match self.revocation_url.as_ref() {
917+
Some(url) if url.url().scheme() == "https" => Ok(url),
918+
Some(_) => Err(ConfigurationError::InsecureUrl("revocation")),
919+
None => Err(ConfigurationError::MissingUrl("revocation")),
920+
}?;
921+
922+
Ok(RevocationRequest {
883923
auth_type: &self.auth_type,
884924
client_id: &self.client_id,
885925
client_secret: self.client_secret.as_ref(),
886926
extra_params: Vec::new(),
887-
revocation_url: self.revocation_url.as_ref(),
927+
revocation_url,
888928
token,
889929
_phantom: PhantomData,
890-
}
930+
})
891931
}
892932
}
893933

@@ -1529,7 +1569,7 @@ where
15291569
client_id: &'a ClientId,
15301570
client_secret: Option<&'a ClientSecret>,
15311571
extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
1532-
introspection_url: Option<&'a IntrospectionUrl>,
1572+
introspection_url: &'a IntrospectionUrl,
15331573

15341574
_phantom: PhantomData<(TE, TIR, TT)>,
15351575
}
@@ -1604,11 +1644,7 @@ where
16041644
&self.extra_params,
16051645
None,
16061646
None,
1607-
self.introspection_url
1608-
.ok_or_else(|| {
1609-
RequestTokenError::Other("no introspection_url provided".to_string())
1610-
})?
1611-
.url(),
1647+
self.introspection_url.url(),
16121648
params,
16131649
))
16141650
}
@@ -1662,7 +1698,7 @@ where
16621698
client_id: &'a ClientId,
16631699
client_secret: Option<&'a ClientSecret>,
16641700
extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
1665-
revocation_url: Option<&'a RevocationUrl>,
1701+
revocation_url: &'a RevocationUrl,
16661702

16671703
_phantom: PhantomData<(RT, TE)>,
16681704
}
@@ -1700,21 +1736,6 @@ where
17001736
where
17011737
RE: Error + 'static,
17021738
{
1703-
// https://tools.ietf.org/html/rfc7009#section-2 states:
1704-
// "The client requests the revocation of a particular token by making an
1705-
// HTTP POST request to the token revocation endpoint URL. This URL
1706-
// MUST conform to the rules given in [RFC6749], Section 3.1. Clients
1707-
// MUST verify that the URL is an HTTPS URL."
1708-
let revocation_url = match self.revocation_url {
1709-
Some(url) if url.url().scheme() == "https" => Ok(url.url()),
1710-
Some(_) => Err(RequestTokenError::Other(
1711-
"revocation_url is not HTTPS".to_string(),
1712-
)),
1713-
None => Err(RequestTokenError::Other(
1714-
"no revocation_url provided".to_string(),
1715-
)),
1716-
}?;
1717-
17181739
let mut params: Vec<(&str, &str)> = vec![("token", self.token.secret())];
17191740
if let Some(type_hint) = self.token.type_hint() {
17201741
params.push(("token_type_hint", type_hint));
@@ -1727,7 +1748,7 @@ where
17271748
&self.extra_params,
17281749
None,
17291750
None,
1730-
revocation_url,
1751+
self.revocation_url.url(),
17311752
params,
17321753
))
17331754
}
@@ -1972,7 +1993,7 @@ where
19721993
client_secret: Option<&'a ClientSecret>,
19731994
extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
19741995
scopes: Vec<Cow<'a, Scope>>,
1975-
device_authorization_url: Option<&'a DeviceAuthorizationUrl>,
1996+
device_authorization_url: &'a DeviceAuthorizationUrl,
19761997
_phantom: PhantomData<TE>,
19771998
}
19781999

@@ -2023,11 +2044,7 @@ where
20232044
&self.extra_params,
20242045
None,
20252046
Some(&self.scopes),
2026-
self.device_authorization_url
2027-
.ok_or_else(|| {
2028-
RequestTokenError::Other("no device authorization_url provided".to_string())
2029-
})?
2030-
.url(),
2047+
self.device_authorization_url.url(),
20312048
vec![],
20322049
))
20332050
}

src/revocation.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub trait RevocableToken {
2020
/// Indicates the type of the token being revoked, as defined by [RFC 7009, Section 2.1](https://tools.ietf.org/html/rfc7009#section-2.1).
2121
///
2222
/// Implementations should return `Some(...)` values for token types that the target authorization servers are
23-
/// expected to know (e.g. because they are registered in the [OAuth Token Type Hints Registry](https://tools.ietf.org/html/rfc7009#section-4.1.2))
23+
/// expected to know (e.g. because they are registered in the [OAuth Token Type Hints Registry](https://tools.ietf.org/html/rfc7009#section-4.1.2)
2424
/// so that they can potentially optimize their search for the token to be revoked.
2525
///
2626
fn type_hint(&self) -> Option<&str>;

src/tests.rs

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,6 +1542,7 @@ fn test_token_introspection_successful_with_basic_auth_minimal_response() {
15421542

15431543
let introspection_response = client
15441544
.introspect(&AccessToken::new("access_token_123".to_string()))
1545+
.unwrap()
15451546
.request(mock_http_client(
15461547
vec![
15471548
(ACCEPT, "application/json"),
@@ -1592,6 +1593,7 @@ fn test_token_introspection_successful_with_basic_auth_full_response() {
15921593

15931594
let introspection_response = client
15941595
.introspect(&AccessToken::new("access_token_123".to_string()))
1596+
.unwrap()
15951597
.set_token_type_hint("access_token")
15961598
.request(mock_http_client(
15971599
vec![
@@ -1674,35 +1676,29 @@ fn test_token_introspection_successful_with_basic_auth_full_response() {
16741676
fn test_token_revocation_with_missing_url() {
16751677
let client = new_client();
16761678

1677-
type TestError = RequestTokenError<std::fmt::Error, BasicRevocationErrorResponse>;
1678-
type TestResult = Result<(), TestError>;
1679-
1680-
let result: TestResult = client
1679+
let result = client
16811680
.revoke_token(AccessToken::new("access_token_123".to_string()).into())
1682-
.request(|_| unreachable!());
1681+
.unwrap_err();
16831682

1684-
match result {
1685-
Err(RequestTokenError::Other(msg)) => assert_eq!(msg, "no revocation_url provided"),
1686-
_ => unreachable!("Expected an error"),
1687-
};
1683+
assert_eq!(
1684+
format!("{}", result),
1685+
"No revocation endpoint URL specified"
1686+
);
16881687
}
16891688

16901689
#[test]
16911690
fn test_token_revocation_with_non_https_url() {
16921691
let client = new_client();
16931692

1694-
type TestError = RequestTokenError<std::fmt::Error, BasicRevocationErrorResponse>;
1695-
type TestResult = Result<(), TestError>;
1696-
1697-
let result: TestResult = client
1693+
let result = client
16981694
.set_revocation_url(RevocationUrl::new("http://revocation/url".to_string()).unwrap())
16991695
.revoke_token(AccessToken::new("access_token_123".to_string()).into())
1700-
.request(|_| unreachable!());
1696+
.unwrap_err();
17011697

1702-
match result {
1703-
Err(RequestTokenError::Other(msg)) => assert_eq!(msg, "revocation_url is not HTTPS"),
1704-
_ => unreachable!("Expected an error"),
1705-
};
1698+
assert_eq!(
1699+
format!("{}", result),
1700+
"Scheme for revocation endpoint URL must be HTTPS"
1701+
);
17061702
}
17071703

17081704
#[test]
@@ -1711,7 +1707,7 @@ fn test_token_revocation_with_unsupported_token_type() {
17111707
.set_revocation_url(RevocationUrl::new("https://revocation/url".to_string()).unwrap());
17121708

17131709
let revocation_response = client
1714-
.revoke_token(AccessToken::new("access_token_123".to_string()).into())
1710+
.revoke_token(AccessToken::new("access_token_123".to_string()).into()).unwrap()
17151711
.request(mock_http_client(
17161712
vec![
17171713
(ACCEPT, "application/json"),
@@ -1752,6 +1748,7 @@ fn test_token_revocation_with_access_token_and_empty_json_response() {
17521748

17531749
client
17541750
.revoke_token(AccessToken::new("access_token_123".to_string()).into())
1751+
.unwrap()
17551752
.request(mock_http_client(
17561753
vec![
17571754
(ACCEPT, "application/json"),
@@ -1781,6 +1778,7 @@ fn test_token_revocation_with_access_token_and_empty_response() {
17811778

17821779
client
17831780
.revoke_token(AccessToken::new("access_token_123".to_string()).into())
1781+
.unwrap()
17841782
.request(mock_http_client(
17851783
vec![
17861784
(ACCEPT, "application/json"),
@@ -1805,6 +1803,7 @@ fn test_token_revocation_with_access_token_and_non_json_response() {
18051803

18061804
client
18071805
.revoke_token(AccessToken::new("access_token_123".to_string()).into())
1806+
.unwrap()
18081807
.request(mock_http_client(
18091808
vec![
18101809
(ACCEPT, "application/json"),
@@ -1834,6 +1833,7 @@ fn test_token_revocation_with_refresh_token() {
18341833

18351834
client
18361835
.revoke_token(RefreshToken::new("refresh_token_123".to_string()).into())
1836+
.unwrap()
18371837
.request(mock_http_client(
18381838
vec![
18391839
(ACCEPT, "application/json"),
@@ -1871,6 +1871,7 @@ fn test_extension_token_revocation_successful() {
18711871
.revoke_token(ColorfulRevocableToken::Red(
18721872
"colorful_token_123".to_string(),
18731873
))
1874+
.unwrap()
18741875
.request(mock_http_client(
18751876
vec![
18761877
(ACCEPT, "application/json"),
@@ -1918,6 +1919,7 @@ fn new_device_auth_details(expires_in: u32) -> StandardDeviceAuthorizationRespon
19181919
let client = new_client().set_device_authorization_url(device_auth_url.clone());
19191920
client
19201921
.exchange_device_code()
1922+
.unwrap()
19211923
.add_extra_param("foo", "bar")
19221924
.add_scope(Scope::new("openid".to_string()))
19231925
.request(mock_http_client(

0 commit comments

Comments
 (0)