1use crate::basic::BasicErrorResponseType;
2use crate::endpoint::{endpoint_request, endpoint_response_status_only};
3use crate::{
4 AccessToken, AsyncHttpClient, AuthType, Client, ClientId, ClientSecret, ConfigurationError,
5 EndpointState, ErrorResponse, ErrorResponseType, HttpRequest, RefreshToken, RequestTokenError,
6 RevocationUrl, SyncHttpClient, TokenIntrospectionResponse, TokenResponse,
7};
8
9use serde::{Deserialize, Serialize};
10
11use std::borrow::Cow;
12use std::error::Error;
13use std::fmt::Error as FormatterError;
14use std::fmt::{Debug, Display, Formatter};
15use std::future::Future;
16use std::marker::PhantomData;
17
18impl<
19 TE,
20 TR,
21 TIR,
22 RT,
23 TRE,
24 HasAuthUrl,
25 HasDeviceAuthUrl,
26 HasIntrospectionUrl,
27 HasRevocationUrl,
28 HasTokenUrl,
29 >
30 Client<
31 TE,
32 TR,
33 TIR,
34 RT,
35 TRE,
36 HasAuthUrl,
37 HasDeviceAuthUrl,
38 HasIntrospectionUrl,
39 HasRevocationUrl,
40 HasTokenUrl,
41 >
42where
43 TE: ErrorResponse + 'static,
44 TR: TokenResponse,
45 TIR: TokenIntrospectionResponse,
46 RT: RevocableToken,
47 TRE: ErrorResponse + 'static,
48 HasAuthUrl: EndpointState,
49 HasDeviceAuthUrl: EndpointState,
50 HasIntrospectionUrl: EndpointState,
51 HasRevocationUrl: EndpointState,
52 HasTokenUrl: EndpointState,
53{
54 pub(crate) fn revoke_token_impl<'a>(
55 &'a self,
56 revocation_url: &'a RevocationUrl,
57 token: RT,
58 ) -> Result<RevocationRequest<'a, RT, TRE>, ConfigurationError> {
59 if revocation_url.url().scheme() != "https" {
65 return Err(ConfigurationError::InsecureUrl("revocation"));
66 }
67
68 Ok(RevocationRequest {
69 auth_type: &self.auth_type,
70 client_id: &self.client_id,
71 client_secret: self.client_secret.as_ref(),
72 extra_params: Vec::new(),
73 revocation_url,
74 token,
75 _phantom: PhantomData,
76 })
77 }
78}
79
80pub trait RevocableToken {
84 fn secret(&self) -> &str;
86
87 fn type_hint(&self) -> Option<&str>;
93}
94
95#[derive(Clone, Debug, Deserialize, Serialize)]
157#[non_exhaustive]
158pub enum StandardRevocableToken {
159 AccessToken(AccessToken),
161 RefreshToken(RefreshToken),
163}
164impl RevocableToken for StandardRevocableToken {
165 fn secret(&self) -> &str {
166 match self {
167 Self::AccessToken(token) => token.secret(),
168 Self::RefreshToken(token) => token.secret(),
169 }
170 }
171
172 fn type_hint(&self) -> Option<&str> {
180 match self {
181 StandardRevocableToken::AccessToken(_) => Some("access_token"),
182 StandardRevocableToken::RefreshToken(_) => Some("refresh_token"),
183 }
184 }
185}
186
187impl From<AccessToken> for StandardRevocableToken {
188 fn from(token: AccessToken) -> Self {
189 Self::AccessToken(token)
190 }
191}
192
193impl From<&AccessToken> for StandardRevocableToken {
194 fn from(token: &AccessToken) -> Self {
195 Self::AccessToken(token.clone())
196 }
197}
198
199impl From<RefreshToken> for StandardRevocableToken {
200 fn from(token: RefreshToken) -> Self {
201 Self::RefreshToken(token)
202 }
203}
204
205impl From<&RefreshToken> for StandardRevocableToken {
206 fn from(token: &RefreshToken) -> Self {
207 Self::RefreshToken(token.clone())
208 }
209}
210
211#[derive(Debug)]
214pub struct RevocationRequest<'a, RT, TE>
215where
216 RT: RevocableToken,
217 TE: ErrorResponse,
218{
219 pub(crate) token: RT,
220 pub(crate) auth_type: &'a AuthType,
221 pub(crate) client_id: &'a ClientId,
222 pub(crate) client_secret: Option<&'a ClientSecret>,
223 pub(crate) extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
224 pub(crate) revocation_url: &'a RevocationUrl,
225 pub(crate) _phantom: PhantomData<(RT, TE)>,
226}
227
228impl<'a, RT, TE> RevocationRequest<'a, RT, TE>
229where
230 RT: RevocableToken,
231 TE: ErrorResponse + 'static,
232{
233 pub fn add_extra_param<N, V>(mut self, name: N, value: V) -> Self
247 where
248 N: Into<Cow<'a, str>>,
249 V: Into<Cow<'a, str>>,
250 {
251 self.extra_params.push((name.into(), value.into()));
252 self
253 }
254
255 fn prepare_request<RE>(self) -> Result<HttpRequest, RequestTokenError<RE, TE>>
256 where
257 RE: Error + 'static,
258 {
259 let mut params: Vec<(&str, &str)> = vec![("token", self.token.secret())];
260 if let Some(type_hint) = self.token.type_hint() {
261 params.push(("token_type_hint", type_hint));
262 }
263
264 endpoint_request(
265 self.auth_type,
266 self.client_id,
267 self.client_secret,
268 &self.extra_params,
269 None,
270 None,
271 self.revocation_url.url(),
272 params,
273 )
274 .map_err(|err| RequestTokenError::Other(format!("failed to prepare request: {err}")))
275 }
276
277 pub fn request<C>(
285 self,
286 http_client: &C,
287 ) -> Result<(), RequestTokenError<<C as SyncHttpClient>::Error, TE>>
288 where
289 C: SyncHttpClient,
290 {
291 endpoint_response_status_only(http_client.call(self.prepare_request()?)?)
295 }
296
297 pub fn request_async<'c, C>(
299 self,
300 http_client: &'c C,
301 ) -> impl Future<Output = Result<(), RequestTokenError<<C as AsyncHttpClient<'c>>::Error, TE>>> + 'c
302 where
303 Self: 'c,
304 C: AsyncHttpClient<'c>,
305 {
306 Box::pin(async move {
307 endpoint_response_status_only(http_client.call(self.prepare_request()?).await?)
308 })
309 }
310}
311
312#[derive(Clone, PartialEq, Eq)]
318pub enum RevocationErrorResponseType {
319 UnsupportedTokenType,
321 Basic(BasicErrorResponseType),
323}
324impl RevocationErrorResponseType {
325 fn from_str(s: &str) -> Self {
326 match BasicErrorResponseType::from_str(s) {
327 BasicErrorResponseType::Extension(ext) => match ext.as_str() {
328 "unsupported_token_type" => RevocationErrorResponseType::UnsupportedTokenType,
329 _ => RevocationErrorResponseType::Basic(BasicErrorResponseType::Extension(ext)),
330 },
331 basic => RevocationErrorResponseType::Basic(basic),
332 }
333 }
334}
335impl AsRef<str> for RevocationErrorResponseType {
336 fn as_ref(&self) -> &str {
337 match self {
338 RevocationErrorResponseType::UnsupportedTokenType => "unsupported_token_type",
339 RevocationErrorResponseType::Basic(basic) => basic.as_ref(),
340 }
341 }
342}
343impl<'de> serde::Deserialize<'de> for RevocationErrorResponseType {
344 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
345 where
346 D: serde::de::Deserializer<'de>,
347 {
348 let variant_str = String::deserialize(deserializer)?;
349 Ok(Self::from_str(&variant_str))
350 }
351}
352impl serde::ser::Serialize for RevocationErrorResponseType {
353 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
354 where
355 S: serde::ser::Serializer,
356 {
357 serializer.serialize_str(self.as_ref())
358 }
359}
360impl ErrorResponseType for RevocationErrorResponseType {}
361impl Debug for RevocationErrorResponseType {
362 fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> {
363 Display::fmt(self, f)
364 }
365}
366
367impl Display for RevocationErrorResponseType {
368 fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> {
369 write!(f, "{}", self.as_ref())
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use crate::basic::BasicRevocationErrorResponse;
376 use crate::tests::colorful_extension::{ColorfulClient, ColorfulRevocableToken};
377 use crate::tests::{mock_http_client, new_client};
378 use crate::{
379 AccessToken, AuthUrl, ClientId, ClientSecret, RefreshToken, RequestTokenError,
380 RevocationErrorResponseType, RevocationUrl, TokenUrl,
381 };
382
383 use http::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
384 use http::{HeaderValue, Response, StatusCode};
385
386 #[test]
387 fn test_token_revocation_with_missing_url() {
388 let client = new_client().set_revocation_url_option(None);
389
390 let result = client
391 .revoke_token(AccessToken::new("access_token_123".to_string()).into())
392 .unwrap_err();
393
394 assert_eq!(result.to_string(), "No revocation endpoint URL specified");
395 }
396
397 #[test]
398 fn test_token_revocation_with_non_https_url() {
399 let client = new_client();
400
401 let result = client
402 .set_revocation_url(RevocationUrl::new("http://revocation/url".to_string()).unwrap())
403 .revoke_token(AccessToken::new("access_token_123".to_string()).into())
404 .unwrap_err();
405
406 assert_eq!(
407 result.to_string(),
408 "Scheme for revocation endpoint URL must be HTTPS"
409 );
410 }
411
412 #[test]
413 fn test_token_revocation_with_unsupported_token_type() {
414 let client = new_client()
415 .set_revocation_url(RevocationUrl::new("https://revocation/url".to_string()).unwrap());
416
417 let revocation_response = client
418 .revoke_token(AccessToken::new("access_token_123".to_string()).into()).unwrap()
419 .request(&mock_http_client(
420 vec![
421 (ACCEPT, "application/json"),
422 (CONTENT_TYPE, "application/x-www-form-urlencoded"),
423 (AUTHORIZATION, "Basic YWFhOmJiYg=="),
424 ],
425 "token=access_token_123&token_type_hint=access_token",
426 Some("https://revocation/url".parse().unwrap()),
427 Response::builder()
428 .status(StatusCode::BAD_REQUEST)
429 .header(
430 CONTENT_TYPE,
431 HeaderValue::from_str("application/json").unwrap(),
432 )
433 .body(
434 "{\
435 \"error\": \"unsupported_token_type\", \"error_description\": \"stuff happened\", \
436 \"error_uri\": \"https://errors\"\
437 }"
438 .to_string()
439 .into_bytes(),
440 )
441 .unwrap(),
442 ));
443
444 assert!(matches!(
445 revocation_response,
446 Err(RequestTokenError::ServerResponse(
447 BasicRevocationErrorResponse {
448 error: RevocationErrorResponseType::UnsupportedTokenType,
449 ..
450 }
451 ))
452 ));
453 }
454
455 #[test]
456 fn test_token_revocation_with_access_token_and_empty_json_response() {
457 let client = new_client()
458 .set_revocation_url(RevocationUrl::new("https://revocation/url".to_string()).unwrap());
459
460 client
461 .revoke_token(AccessToken::new("access_token_123".to_string()).into())
462 .unwrap()
463 .request(&mock_http_client(
464 vec![
465 (ACCEPT, "application/json"),
466 (CONTENT_TYPE, "application/x-www-form-urlencoded"),
467 (AUTHORIZATION, "Basic YWFhOmJiYg=="),
468 ],
469 "token=access_token_123&token_type_hint=access_token",
470 Some("https://revocation/url".parse().unwrap()),
471 Response::builder()
472 .status(StatusCode::OK)
473 .header(
474 CONTENT_TYPE,
475 HeaderValue::from_str("application/json").unwrap(),
476 )
477 .body(b"{}".to_vec())
478 .unwrap(),
479 ))
480 .unwrap();
481 }
482
483 #[test]
484 fn test_token_revocation_with_access_token_and_empty_response() {
485 let client = new_client()
486 .set_revocation_url(RevocationUrl::new("https://revocation/url".to_string()).unwrap());
487
488 client
489 .revoke_token(AccessToken::new("access_token_123".to_string()).into())
490 .unwrap()
491 .request(&mock_http_client(
492 vec![
493 (ACCEPT, "application/json"),
494 (CONTENT_TYPE, "application/x-www-form-urlencoded"),
495 (AUTHORIZATION, "Basic YWFhOmJiYg=="),
496 ],
497 "token=access_token_123&token_type_hint=access_token",
498 Some("https://revocation/url".parse().unwrap()),
499 Response::builder()
500 .status(StatusCode::OK)
501 .body(vec![])
502 .unwrap(),
503 ))
504 .unwrap();
505 }
506
507 #[test]
508 fn test_token_revocation_with_access_token_and_non_json_response() {
509 let client = new_client()
510 .set_revocation_url(RevocationUrl::new("https://revocation/url".to_string()).unwrap());
511
512 client
513 .revoke_token(AccessToken::new("access_token_123".to_string()).into())
514 .unwrap()
515 .request(&mock_http_client(
516 vec![
517 (ACCEPT, "application/json"),
518 (CONTENT_TYPE, "application/x-www-form-urlencoded"),
519 (AUTHORIZATION, "Basic YWFhOmJiYg=="),
520 ],
521 "token=access_token_123&token_type_hint=access_token",
522 Some("https://revocation/url".parse().unwrap()),
523 Response::builder()
524 .status(StatusCode::OK)
525 .header(
526 CONTENT_TYPE,
527 HeaderValue::from_str("application/octet-stream").unwrap(),
528 )
529 .body(vec![1, 2, 3])
530 .unwrap(),
531 ))
532 .unwrap();
533 }
534
535 #[test]
536 fn test_token_revocation_with_refresh_token() {
537 let client = new_client()
538 .set_revocation_url(RevocationUrl::new("https://revocation/url".to_string()).unwrap());
539
540 client
541 .revoke_token(RefreshToken::new("refresh_token_123".to_string()).into())
542 .unwrap()
543 .request(&mock_http_client(
544 vec![
545 (ACCEPT, "application/json"),
546 (CONTENT_TYPE, "application/x-www-form-urlencoded"),
547 (AUTHORIZATION, "Basic YWFhOmJiYg=="),
548 ],
549 "token=refresh_token_123&token_type_hint=refresh_token",
550 Some("https://revocation/url".parse().unwrap()),
551 Response::builder()
552 .status(StatusCode::OK)
553 .header(
554 CONTENT_TYPE,
555 HeaderValue::from_str("application/json").unwrap(),
556 )
557 .body(b"{}".to_vec())
558 .unwrap(),
559 ))
560 .unwrap();
561 }
562
563 #[test]
564 fn test_extension_token_revocation_successful() {
565 let client = ColorfulClient::new(ClientId::new("aaa".to_string()))
566 .set_client_secret(ClientSecret::new("bbb".to_string()))
567 .set_auth_uri(AuthUrl::new("https://example.com/auth".to_string()).unwrap())
568 .set_token_uri(TokenUrl::new("https://example.com/token".to_string()).unwrap())
569 .set_revocation_url(RevocationUrl::new("https://revocation/url".to_string()).unwrap());
570
571 client
572 .revoke_token(ColorfulRevocableToken::Red(
573 "colorful_token_123".to_string(),
574 ))
575 .unwrap()
576 .request(&mock_http_client(
577 vec![
578 (ACCEPT, "application/json"),
579 (CONTENT_TYPE, "application/x-www-form-urlencoded"),
580 (AUTHORIZATION, "Basic YWFhOmJiYg=="),
581 ],
582 "token=colorful_token_123&token_type_hint=red_token",
583 Some("https://revocation/url".parse().unwrap()),
584 Response::builder()
585 .status(StatusCode::OK)
586 .header(
587 CONTENT_TYPE,
588 HeaderValue::from_str("application/json").unwrap(),
589 )
590 .body(b"{}".to_vec())
591 .unwrap(),
592 ))
593 .unwrap();
594 }
595}