oauth2/
introspection.rs

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/// A request to introspect an access token.
74///
75/// See <https://tools.ietf.org/html/rfc7662#section-2.1>.
76#[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    /// Sets the optional token_type_hint parameter.
98    ///
99    /// See <https://tools.ietf.org/html/rfc7662#section-2.1>.
100    ///
101    /// OPTIONAL.  A hint about the type of the token submitted for
102    ///       introspection.  The protected resource MAY pass this parameter to
103    ///       help the authorization server optimize the token lookup.  If the
104    ///       server is unable to locate the token using the given hint, it MUST
105    ///      extend its search across all of its supported token types.  An
106    ///      authorization server MAY ignore this parameter, particularly if it
107    ///      is able to detect the token type automatically.  Values for this
108    ///      field are defined in the "OAuth Token Type Hints" registry defined
109    ///      in OAuth Token Revocation [RFC7009](https://tools.ietf.org/html/rfc7009).
110    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    /// Appends an extra param to the token introspection request.
120    ///
121    /// This method allows extensions to be used without direct support from
122    /// this crate. If `name` conflicts with a parameter managed by this crate, the
123    /// behavior is undefined. In particular, do not set parameters defined by
124    /// [RFC 6749](https://tools.ietf.org/html/rfc6749) or
125    /// [RFC 7662](https://tools.ietf.org/html/rfc7662).
126    ///
127    /// # Security Warning
128    ///
129    /// Callers should follow the security recommendations for any OAuth2 extensions used with
130    /// this function, which are beyond the scope of
131    /// [RFC 6749](https://tools.ietf.org/html/rfc6749).
132    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    /// Synchronously sends the request to the authorization server and awaits a response.
164    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    /// Asynchronously sends the request to the authorization server and returns a Future.
175    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
187/// Common methods shared by all OAuth2 token introspection implementations.
188///
189/// The methods in this trait are defined in
190/// [Section 2.2 of RFC 7662](https://tools.ietf.org/html/rfc7662#section-2.2). This trait exists
191/// separately from the `StandardTokenIntrospectionResponse` struct to support customization by
192/// clients, such as supporting interoperability with non-standards-complaint OAuth2 providers.
193pub trait TokenIntrospectionResponse: Debug + DeserializeOwned + Serialize {
194    /// Type of OAuth2 access token included in this response.
195    type TokenType: TokenType;
196
197    /// REQUIRED.  Boolean indicator of whether or not the presented token
198    /// is currently active.  The specifics of a token's "active" state
199    /// will vary depending on the implementation of the authorization
200    /// server and the information it keeps about its tokens, but a "true"
201    /// value return for the "active" property will generally indicate
202    /// that a given token has been issued by this authorization server,
203    /// has not been revoked by the resource owner, and is within its
204    /// given time window of validity (e.g., after its issuance time and
205    /// before its expiration time).
206    fn active(&self) -> bool;
207    /// OPTIONAL.  A JSON string containing a space-separated list of
208    /// scopes associated with this token, in the format described in
209    /// [Section 3.3 of RFC 7662](https://tools.ietf.org/html/rfc7662#section-3.3).
210    /// If included in the response,
211    /// this space-delimited field is parsed into a `Vec` of individual scopes. If omitted from
212    /// the response, this field is `None`.
213    fn scopes(&self) -> Option<&Vec<Scope>>;
214    /// OPTIONAL.  Client identifier for the OAuth 2.0 client that
215    /// requested this token.
216    fn client_id(&self) -> Option<&ClientId>;
217    /// OPTIONAL.  Human-readable identifier for the resource owner who
218    /// authorized this token.
219    fn username(&self) -> Option<&str>;
220    /// OPTIONAL.  Type of the token as defined in
221    /// [Section 5.1 of RFC 7662](https://tools.ietf.org/html/rfc7662#section-5.1).
222    /// Value is case insensitive and deserialized to the generic `TokenType` parameter.
223    fn token_type(&self) -> Option<&Self::TokenType>;
224    /// OPTIONAL.  Integer timestamp, measured in the number of seconds
225    /// since January 1 1970 UTC, indicating when this token will expire,
226    /// as defined in JWT [RFC7519](https://tools.ietf.org/html/rfc7519).
227    fn exp(&self) -> Option<DateTime<Utc>>;
228    /// OPTIONAL.  Integer timestamp, measured in the number of seconds
229    /// since January 1 1970 UTC, indicating when this token was
230    /// originally issued, as defined in JWT [RFC7519](https://tools.ietf.org/html/rfc7519).
231    fn iat(&self) -> Option<DateTime<Utc>>;
232    /// OPTIONAL.  Integer timestamp, measured in the number of seconds
233    /// since January 1 1970 UTC, indicating when this token is not to be
234    /// used before, as defined in JWT [RFC7519](https://tools.ietf.org/html/rfc7519).
235    fn nbf(&self) -> Option<DateTime<Utc>>;
236    /// OPTIONAL.  Subject of the token, as defined in JWT [RFC7519](https://tools.ietf.org/html/rfc7519).
237    /// Usually a machine-readable identifier of the resource owner who
238    /// authorized this token.
239    fn sub(&self) -> Option<&str>;
240    /// OPTIONAL.  Service-specific string identifier or list of string
241    /// identifiers representing the intended audience for this token, as
242    /// defined in JWT [RFC7519](https://tools.ietf.org/html/rfc7519).
243    fn aud(&self) -> Option<&Vec<String>>;
244    /// OPTIONAL.  String representing the issuer of this token, as
245    /// defined in JWT [RFC7519](https://tools.ietf.org/html/rfc7519).
246    fn iss(&self) -> Option<&str>;
247    /// OPTIONAL.  String identifier for the token, as defined in JWT
248    /// [RFC7519](https://tools.ietf.org/html/rfc7519).
249    fn jti(&self) -> Option<&str>;
250}
251
252/// Standard OAuth2 token introspection response.
253///
254/// This struct includes the fields defined in
255/// [Section 2.2 of RFC 7662](https://tools.ietf.org/html/rfc7662#section-2.2), as well as
256/// extensions defined by the `EF` type parameter.
257#[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    /// Instantiate a new OAuth2 token introspection response.
319    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    /// Sets the `set_active` field.
339    pub fn set_active(&mut self, active: bool) {
340        self.active = active;
341    }
342    /// Sets the `set_scopes` field.
343    pub fn set_scopes(&mut self, scopes: Option<Vec<Scope>>) {
344        self.scopes = scopes;
345    }
346    /// Sets the `set_client_id` field.
347    pub fn set_client_id(&mut self, client_id: Option<ClientId>) {
348        self.client_id = client_id;
349    }
350    /// Sets the `set_username` field.
351    pub fn set_username(&mut self, username: Option<String>) {
352        self.username = username;
353    }
354    /// Sets the `set_token_type` field.
355    pub fn set_token_type(&mut self, token_type: Option<TT>) {
356        self.token_type = token_type;
357    }
358    /// Sets the `set_exp` field.
359    pub fn set_exp(&mut self, exp: Option<DateTime<Utc>>) {
360        self.exp = exp;
361    }
362    /// Sets the `set_iat` field.
363    pub fn set_iat(&mut self, iat: Option<DateTime<Utc>>) {
364        self.iat = iat;
365    }
366    /// Sets the `set_nbf` field.
367    pub fn set_nbf(&mut self, nbf: Option<DateTime<Utc>>) {
368        self.nbf = nbf;
369    }
370    /// Sets the `set_sub` field.
371    pub fn set_sub(&mut self, sub: Option<String>) {
372        self.sub = sub;
373    }
374    /// Sets the `set_aud` field.
375    pub fn set_aud(&mut self, aud: Option<Vec<String>>) {
376        self.aud = aud;
377    }
378    /// Sets the `set_iss` field.
379    pub fn set_iss(&mut self, iss: Option<String>) {
380        self.iss = iss;
381    }
382    /// Sets the `set_jti` field.
383    pub fn set_jti(&mut self, jti: Option<String>) {
384        self.jti = jti;
385    }
386    /// Extra fields defined by the client application.
387    pub fn extra_fields(&self) -> &EF {
388        &self.extra_fields
389    }
390    /// Sets the `set_extra_fields` field.
391    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}