1use crate::endpoint::{endpoint_request, endpoint_response};
2use crate::{
3 AccessToken, AsyncHttpClient, AuthType, Client, ClientId, ClientSecret, EndpointState,
4 ErrorResponse, ExtraTokenFields, HttpRequest, IntrospectionUrl, RequestTokenError,
5 RevocableToken, Scope, SyncHttpClient, TokenResponse, TokenType,
6};
7
8use chrono::serde::ts_seconds_option;
9use chrono::{DateTime, Utc};
10use serde::de::DeserializeOwned;
11use serde::{Deserialize, Serialize};
12
13use std::borrow::Cow;
14use std::error::Error;
15use std::fmt::Debug;
16use std::future::Future;
17use std::marker::PhantomData;
18
19impl<
20 TE,
21 TR,
22 TIR,
23 RT,
24 TRE,
25 HasAuthUrl,
26 HasDeviceAuthUrl,
27 HasIntrospectionUrl,
28 HasRevocationUrl,
29 HasTokenUrl,
30 >
31 Client<
32 TE,
33 TR,
34 TIR,
35 RT,
36 TRE,
37 HasAuthUrl,
38 HasDeviceAuthUrl,
39 HasIntrospectionUrl,
40 HasRevocationUrl,
41 HasTokenUrl,
42 >
43where
44 TE: ErrorResponse + 'static,
45 TR: TokenResponse,
46 TIR: TokenIntrospectionResponse,
47 RT: RevocableToken,
48 TRE: ErrorResponse + 'static,
49 HasAuthUrl: EndpointState,
50 HasDeviceAuthUrl: EndpointState,
51 HasIntrospectionUrl: EndpointState,
52 HasRevocationUrl: EndpointState,
53 HasTokenUrl: EndpointState,
54{
55 pub(crate) fn introspect_impl<'a>(
56 &'a self,
57 introspection_url: &'a IntrospectionUrl,
58 token: &'a AccessToken,
59 ) -> IntrospectionRequest<'a, TE, TIR> {
60 IntrospectionRequest {
61 auth_type: &self.auth_type,
62 client_id: &self.client_id,
63 client_secret: self.client_secret.as_ref(),
64 extra_params: Vec::new(),
65 introspection_url,
66 token,
67 token_type_hint: None,
68 _phantom: PhantomData,
69 }
70 }
71}
72
73#[derive(Debug)]
77pub struct IntrospectionRequest<'a, TE, TIR>
78where
79 TE: ErrorResponse,
80 TIR: TokenIntrospectionResponse,
81{
82 pub(crate) token: &'a AccessToken,
83 pub(crate) token_type_hint: Option<Cow<'a, str>>,
84 pub(crate) auth_type: &'a AuthType,
85 pub(crate) client_id: &'a ClientId,
86 pub(crate) client_secret: Option<&'a ClientSecret>,
87 pub(crate) extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
88 pub(crate) introspection_url: &'a IntrospectionUrl,
89 pub(crate) _phantom: PhantomData<(TE, TIR)>,
90}
91
92impl<'a, TE, TIR> IntrospectionRequest<'a, TE, TIR>
93where
94 TE: ErrorResponse + 'static,
95 TIR: TokenIntrospectionResponse,
96{
97 pub fn set_token_type_hint<V>(mut self, value: V) -> Self
111 where
112 V: Into<Cow<'a, str>>,
113 {
114 self.token_type_hint = Some(value.into());
115
116 self
117 }
118
119 pub fn add_extra_param<N, V>(mut self, name: N, value: V) -> Self
133 where
134 N: Into<Cow<'a, str>>,
135 V: Into<Cow<'a, str>>,
136 {
137 self.extra_params.push((name.into(), value.into()));
138 self
139 }
140
141 fn prepare_request<RE>(self) -> Result<HttpRequest, RequestTokenError<RE, TE>>
142 where
143 RE: Error + 'static,
144 {
145 let mut params: Vec<(&str, &str)> = vec![("token", self.token.secret())];
146 if let Some(ref token_type_hint) = self.token_type_hint {
147 params.push(("token_type_hint", token_type_hint));
148 }
149
150 endpoint_request(
151 self.auth_type,
152 self.client_id,
153 self.client_secret,
154 &self.extra_params,
155 None,
156 None,
157 self.introspection_url.url(),
158 params,
159 )
160 .map_err(|err| RequestTokenError::Other(format!("failed to prepare request: {err}")))
161 }
162
163 pub fn request<C>(
165 self,
166 http_client: &C,
167 ) -> Result<TIR, RequestTokenError<<C as SyncHttpClient>::Error, TE>>
168 where
169 C: SyncHttpClient,
170 {
171 endpoint_response(http_client.call(self.prepare_request()?)?)
172 }
173
174 pub fn request_async<'c, C>(
176 self,
177 http_client: &'c C,
178 ) -> impl Future<Output = Result<TIR, RequestTokenError<<C as AsyncHttpClient<'c>>::Error, TE>>> + 'c
179 where
180 Self: 'c,
181 C: AsyncHttpClient<'c>,
182 {
183 Box::pin(async move { endpoint_response(http_client.call(self.prepare_request()?).await?) })
184 }
185}
186
187pub trait TokenIntrospectionResponse: Debug + DeserializeOwned + Serialize {
194 type TokenType: TokenType;
196
197 fn active(&self) -> bool;
207 fn scopes(&self) -> Option<&Vec<Scope>>;
214 fn client_id(&self) -> Option<&ClientId>;
217 fn username(&self) -> Option<&str>;
220 fn token_type(&self) -> Option<&Self::TokenType>;
224 fn exp(&self) -> Option<DateTime<Utc>>;
228 fn iat(&self) -> Option<DateTime<Utc>>;
232 fn nbf(&self) -> Option<DateTime<Utc>>;
236 fn sub(&self) -> Option<&str>;
240 fn aud(&self) -> Option<&Vec<String>>;
244 fn iss(&self) -> Option<&str>;
247 fn jti(&self) -> Option<&str>;
250}
251
252#[derive(Clone, Debug, Deserialize, Serialize)]
258pub struct StandardTokenIntrospectionResponse<EF, TT>
259where
260 EF: ExtraTokenFields,
261 TT: TokenType + 'static,
262{
263 active: bool,
264 #[serde(rename = "scope")]
265 #[serde(deserialize_with = "crate::helpers::deserialize_space_delimited_vec")]
266 #[serde(serialize_with = "crate::helpers::serialize_space_delimited_vec")]
267 #[serde(skip_serializing_if = "Option::is_none")]
268 #[serde(default)]
269 scopes: Option<Vec<Scope>>,
270 #[serde(skip_serializing_if = "Option::is_none")]
271 client_id: Option<ClientId>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 username: Option<String>,
274 #[serde(
275 bound = "TT: TokenType",
276 skip_serializing_if = "Option::is_none",
277 deserialize_with = "crate::helpers::deserialize_untagged_enum_case_insensitive",
278 default = "none_field"
279 )]
280 token_type: Option<TT>,
281 #[serde(skip_serializing_if = "Option::is_none")]
282 #[serde(with = "ts_seconds_option")]
283 #[serde(default)]
284 exp: Option<DateTime<Utc>>,
285 #[serde(skip_serializing_if = "Option::is_none")]
286 #[serde(with = "ts_seconds_option")]
287 #[serde(default)]
288 iat: Option<DateTime<Utc>>,
289 #[serde(skip_serializing_if = "Option::is_none")]
290 #[serde(with = "ts_seconds_option")]
291 #[serde(default)]
292 nbf: Option<DateTime<Utc>>,
293 #[serde(skip_serializing_if = "Option::is_none")]
294 sub: Option<String>,
295 #[serde(skip_serializing_if = "Option::is_none")]
296 #[serde(default)]
297 #[serde(deserialize_with = "crate::helpers::deserialize_optional_string_or_vec_string")]
298 aud: Option<Vec<String>>,
299 #[serde(skip_serializing_if = "Option::is_none")]
300 iss: Option<String>,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 jti: Option<String>,
303
304 #[serde(bound = "EF: ExtraTokenFields")]
305 #[serde(flatten)]
306 extra_fields: EF,
307}
308
309fn none_field<T>() -> Option<T> {
310 None
311}
312
313impl<EF, TT> StandardTokenIntrospectionResponse<EF, TT>
314where
315 EF: ExtraTokenFields,
316 TT: TokenType,
317{
318 pub fn new(active: bool, extra_fields: EF) -> Self {
320 Self {
321 active,
322
323 scopes: None,
324 client_id: None,
325 username: None,
326 token_type: None,
327 exp: None,
328 iat: None,
329 nbf: None,
330 sub: None,
331 aud: None,
332 iss: None,
333 jti: None,
334 extra_fields,
335 }
336 }
337
338 pub fn set_active(&mut self, active: bool) {
340 self.active = active;
341 }
342 pub fn set_scopes(&mut self, scopes: Option<Vec<Scope>>) {
344 self.scopes = scopes;
345 }
346 pub fn set_client_id(&mut self, client_id: Option<ClientId>) {
348 self.client_id = client_id;
349 }
350 pub fn set_username(&mut self, username: Option<String>) {
352 self.username = username;
353 }
354 pub fn set_token_type(&mut self, token_type: Option<TT>) {
356 self.token_type = token_type;
357 }
358 pub fn set_exp(&mut self, exp: Option<DateTime<Utc>>) {
360 self.exp = exp;
361 }
362 pub fn set_iat(&mut self, iat: Option<DateTime<Utc>>) {
364 self.iat = iat;
365 }
366 pub fn set_nbf(&mut self, nbf: Option<DateTime<Utc>>) {
368 self.nbf = nbf;
369 }
370 pub fn set_sub(&mut self, sub: Option<String>) {
372 self.sub = sub;
373 }
374 pub fn set_aud(&mut self, aud: Option<Vec<String>>) {
376 self.aud = aud;
377 }
378 pub fn set_iss(&mut self, iss: Option<String>) {
380 self.iss = iss;
381 }
382 pub fn set_jti(&mut self, jti: Option<String>) {
384 self.jti = jti;
385 }
386 pub fn extra_fields(&self) -> &EF {
388 &self.extra_fields
389 }
390 pub fn set_extra_fields(&mut self, extra_fields: EF) {
392 self.extra_fields = extra_fields;
393 }
394}
395impl<EF, TT> TokenIntrospectionResponse for StandardTokenIntrospectionResponse<EF, TT>
396where
397 EF: ExtraTokenFields,
398 TT: TokenType,
399{
400 type TokenType = TT;
401
402 fn active(&self) -> bool {
403 self.active
404 }
405
406 fn scopes(&self) -> Option<&Vec<Scope>> {
407 self.scopes.as_ref()
408 }
409
410 fn client_id(&self) -> Option<&ClientId> {
411 self.client_id.as_ref()
412 }
413
414 fn username(&self) -> Option<&str> {
415 self.username.as_deref()
416 }
417
418 fn token_type(&self) -> Option<&TT> {
419 self.token_type.as_ref()
420 }
421
422 fn exp(&self) -> Option<DateTime<Utc>> {
423 self.exp
424 }
425
426 fn iat(&self) -> Option<DateTime<Utc>> {
427 self.iat
428 }
429
430 fn nbf(&self) -> Option<DateTime<Utc>> {
431 self.nbf
432 }
433
434 fn sub(&self) -> Option<&str> {
435 self.sub.as_deref()
436 }
437
438 fn aud(&self) -> Option<&Vec<String>> {
439 self.aud.as_ref()
440 }
441
442 fn iss(&self) -> Option<&str> {
443 self.iss.as_deref()
444 }
445
446 fn jti(&self) -> Option<&str> {
447 self.jti.as_deref()
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use crate::basic::BasicTokenType;
454 use crate::tests::{mock_http_client, new_client};
455 use crate::{AccessToken, AuthType, ClientId, IntrospectionUrl, RedirectUrl, Scope};
456
457 use chrono::DateTime;
458 use http::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
459 use http::{HeaderValue, Response, StatusCode};
460
461 #[test]
462 fn test_token_introspection_successful_with_basic_auth_minimal_response() {
463 let client = new_client()
464 .set_auth_type(AuthType::BasicAuth)
465 .set_redirect_uri(RedirectUrl::new("https://redirect/here".to_string()).unwrap())
466 .set_introspection_url(
467 IntrospectionUrl::new("https://introspection/url".to_string()).unwrap(),
468 );
469
470 let introspection_response = client
471 .introspect(&AccessToken::new("access_token_123".to_string()))
472 .request(&mock_http_client(
473 vec![
474 (ACCEPT, "application/json"),
475 (CONTENT_TYPE, "application/x-www-form-urlencoded"),
476 (AUTHORIZATION, "Basic YWFhOmJiYg=="),
477 ],
478 "token=access_token_123",
479 Some("https://introspection/url".parse().unwrap()),
480 Response::builder()
481 .status(StatusCode::OK)
482 .header(
483 CONTENT_TYPE,
484 HeaderValue::from_str("application/json").unwrap(),
485 )
486 .body(
487 "{\
488 \"active\": true\
489 }"
490 .to_string()
491 .into_bytes(),
492 )
493 .unwrap(),
494 ))
495 .unwrap();
496
497 assert!(introspection_response.active);
498 assert_eq!(None, introspection_response.scopes);
499 assert_eq!(None, introspection_response.client_id);
500 assert_eq!(None, introspection_response.username);
501 assert_eq!(None, introspection_response.token_type);
502 assert_eq!(None, introspection_response.exp);
503 assert_eq!(None, introspection_response.iat);
504 assert_eq!(None, introspection_response.nbf);
505 assert_eq!(None, introspection_response.sub);
506 assert_eq!(None, introspection_response.aud);
507 assert_eq!(None, introspection_response.iss);
508 assert_eq!(None, introspection_response.jti);
509 }
510
511 #[test]
512 fn test_token_introspection_successful_with_basic_auth_full_response() {
513 let client = new_client()
514 .set_auth_type(AuthType::BasicAuth)
515 .set_redirect_uri(RedirectUrl::new("https://redirect/here".to_string()).unwrap())
516 .set_introspection_url(
517 IntrospectionUrl::new("https://introspection/url".to_string()).unwrap(),
518 );
519
520 let introspection_response = client
521 .introspect(&AccessToken::new("access_token_123".to_string()))
522 .set_token_type_hint("access_token")
523 .request(&mock_http_client(
524 vec![
525 (ACCEPT, "application/json"),
526 (CONTENT_TYPE, "application/x-www-form-urlencoded"),
527 (AUTHORIZATION, "Basic YWFhOmJiYg=="),
528 ],
529 "token=access_token_123&token_type_hint=access_token",
530 Some("https://introspection/url".parse().unwrap()),
531 Response::builder()
532 .status(StatusCode::OK)
533 .header(
534 CONTENT_TYPE,
535 HeaderValue::from_str("application/json").unwrap(),
536 )
537 .body(
538 r#"{
539 "active": true,
540 "scope": "email profile",
541 "client_id": "aaa",
542 "username": "demo",
543 "token_type": "bearer",
544 "exp": 1604073517,
545 "iat": 1604073217,
546 "nbf": 1604073317,
547 "sub": "demo",
548 "aud": "demo",
549 "iss": "http://127.0.0.1:8080/auth/realms/test-realm",
550 "jti": "be1b7da2-fc18-47b3-bdf1-7a4f50bcf53f"
551 }"#
552 .to_string()
553 .into_bytes(),
554 )
555 .unwrap(),
556 ))
557 .unwrap();
558
559 assert!(introspection_response.active);
560 assert_eq!(
561 Some(vec![
562 Scope::new("email".to_string()),
563 Scope::new("profile".to_string())
564 ]),
565 introspection_response.scopes
566 );
567 assert_eq!(
568 Some(ClientId::new("aaa".to_string())),
569 introspection_response.client_id
570 );
571 assert_eq!(Some("demo".to_string()), introspection_response.username);
572 assert_eq!(
573 Some(BasicTokenType::Bearer),
574 introspection_response.token_type
575 );
576 assert_eq!(
577 Some(DateTime::from_timestamp(1604073517, 0).unwrap()),
578 introspection_response.exp
579 );
580 assert_eq!(
581 Some(DateTime::from_timestamp(1604073217, 0).unwrap()),
582 introspection_response.iat
583 );
584 assert_eq!(
585 Some(DateTime::from_timestamp(1604073317, 0).unwrap()),
586 introspection_response.nbf
587 );
588 assert_eq!(Some("demo".to_string()), introspection_response.sub);
589 assert_eq!(Some(vec!["demo".to_string()]), introspection_response.aud);
590 assert_eq!(
591 Some("http://127.0.0.1:8080/auth/realms/test-realm".to_string()),
592 introspection_response.iss
593 );
594 assert_eq!(
595 Some("be1b7da2-fc18-47b3-bdf1-7a4f50bcf53f".to_string()),
596 introspection_response.jti
597 );
598 }
599}