tokkit/
client.rs

1//! Different implementations
2
3use std::env;
4use std::io::Read;
5use std::str;
6use std::sync::Arc;
7use std::time::Duration;
8
9use backoff::{Error as BackoffError, ExponentialBackoff, Operation};
10use failure::ResultExt;
11use reqwest::{StatusCode, Url};
12use reqwest::blocking::{Client, Response};
13use url::ParseError;
14
15use crate::parsers::*;
16use crate::{AccessToken, InitializationError, InitializationResult, TokenInfo};
17use crate::{TokenInfoError, TokenInfoErrorKind, TokenInfoResult, TokenInfoService};
18
19#[cfg(feature = "async")]
20use crate::async_client::AsyncTokenInfoServiceClientLight;
21#[cfg(feature = "metrix")]
22use crate::metrics::metrix::MetrixCollector;
23#[cfg(feature = "async")]
24use crate::metrics::{DevNullMetricsCollector, MetricsCollector};
25#[cfg(feature = "metrix")]
26use metrix::processor::{AggregatesProcessors, ProcessorMount};
27
28/// A builder for a `TokenInfoServiceClient`
29///
30/// # Features
31///
32/// * `async` enables
33///     * `build_async`
34///     * `build_async_with_metrics`
35/// * `async` + `metrix` enables
36///     * `build_async_with_metrix`
37pub struct TokenInfoServiceClientBuilder<P: TokenInfoParser> {
38    pub parser: Option<P>,
39    pub endpoint: Option<String>,
40    pub query_parameter: Option<String>,
41    pub fallback_endpoint: Option<String>,
42}
43
44impl<P> TokenInfoServiceClientBuilder<P>
45where
46    P: TokenInfoParser + Clone + Sync + Send + 'static,
47{
48    /// Create a new `TokenInfoServiceClientBuilder` with the given
49    /// `TokenInfoParser` already set.
50    pub fn new(parser: P) -> Self {
51        let mut builder = Self::default();
52        builder.with_parser(parser);
53        builder
54    }
55
56    /// Sets the `TokenInfoParser`. The `TokenInfoParser` is mandatory.
57    pub fn with_parser(&mut self, parser: P) -> &mut Self {
58        self.parser = Some(parser);
59        self
60    }
61
62    /// Sets the introspection endpoint. The introspection endpoint is
63    /// mandatory.
64    pub fn with_endpoint<T: Into<String>>(&mut self, endpoint: T) -> &mut Self {
65        self.endpoint = Some(endpoint.into());
66        self
67    }
68
69    /// Sets a fallback for the introspection endpoint. The fallback is
70    /// optional.
71    pub fn with_fallback_endpoint<T: Into<String>>(&mut self, endpoint: T) -> &mut Self {
72        self.fallback_endpoint = Some(endpoint.into());
73        self
74    }
75
76    /// Sets the query parameter for the access token.
77    /// If ommitted the access token will be part of the URL.
78    pub fn with_query_parameter<T: Into<String>>(&mut self, parameter: T) -> &mut Self {
79        self.query_parameter = Some(parameter.into());
80        self
81    }
82
83    /// Build the `TokenInfoServiceClient`. Fails if not all mandatory fields
84    /// are set.
85    pub fn build(self) -> InitializationResult<TokenInfoServiceClient> {
86        let parser = if let Some(parser) = self.parser {
87            parser
88        } else {
89            return Err(InitializationError("No token info parser.".into()));
90        };
91
92        let endpoint = if let Some(endpoint) = self.endpoint {
93            endpoint
94        } else {
95            return Err(InitializationError("No endpoint.".into()));
96        };
97
98        TokenInfoServiceClient::new::<P>(
99            &endpoint,
100            self.query_parameter.as_ref().map(|s| &**s),
101            self.fallback_endpoint.as_ref().map(|s| &**s),
102            parser,
103        )
104    }
105
106    /// Build the `AsyncTokenInfoServiceClientLight`. Fails if not all
107    /// mandatory fields are set.
108    #[cfg(feature = "async")]
109    pub fn build_async(
110        self,
111    ) -> InitializationResult<AsyncTokenInfoServiceClientLight<P, DevNullMetricsCollector>> {
112        self.build_async_with_metrics(DevNullMetricsCollector)
113    }
114
115    /// Build the `AsyncTokenInfoServiceClientLight`. Fails if not all
116    /// mandatory fields are set.
117    #[cfg(feature = "async")]
118    pub fn build_async_with_metrics<M>(
119        self,
120        metrics_collector: M,
121    ) -> InitializationResult<AsyncTokenInfoServiceClientLight<P, M>>
122    where
123        M: MetricsCollector + Clone + Send + 'static,
124    {
125        let parser = if let Some(parser) = self.parser {
126            parser
127        } else {
128            return Err(InitializationError("No token info parser.".into()));
129        };
130
131        let endpoint = if let Some(endpoint) = self.endpoint {
132            endpoint
133        } else {
134            return Err(InitializationError("No endpoint.".into()));
135        };
136
137        AsyncTokenInfoServiceClientLight::with_metrics(
138            &endpoint,
139            self.query_parameter.as_ref().map(|s| &**s),
140            self.fallback_endpoint.as_ref().map(|s| &**s),
141            parser,
142            metrics_collector,
143        )
144    }
145
146    /// Build the `AsyncTokenInfoServiceClientLight`. Fails if not all
147    /// mandatory fields are set.
148    ///
149    /// If `group_name` is defined a new group with the given
150    /// name will be created. Otherwise the metrics of the
151    /// client will be directly added to `takes_metrics`.
152    #[cfg(all(feature = "async", feature = "metrix"))]
153    pub fn build_async_with_metrix<M, T>(
154        self,
155        takes_metrics: &mut M,
156        group_name: Option<T>,
157    ) -> InitializationResult<AsyncTokenInfoServiceClientLight<P, MetrixCollector>>
158    where
159        M: AggregatesProcessors,
160        T: Into<String>,
161    {
162        let metrics_collector = if let Some(group) = group_name {
163            let mut mount = ProcessorMount::new(group);
164            let collector = MetrixCollector::new(&mut mount);
165            takes_metrics.add_processor(mount);
166            collector
167        } else {
168            MetrixCollector::new(takes_metrics)
169        };
170
171        self.build_async_with_metrics(metrics_collector)
172    }
173
174    /// Creates a new `TokenInfoServiceClientBuilder` from environment
175    /// parameters.
176    ///
177    /// The following variables used to identify the field in a token info
178    /// response:
179    ///
180    /// * `TOKKIT_TOKEN_INTROSPECTION_ENDPOINT`(mandatory): The endpoint of the
181    /// service * `TOKKIT_TOKEN_INTROSPECTION_QUERY_PARAMETER`(optional):
182    /// The request parameter
183    /// * `TOKKIT_TOKEN_INTROSPECTION_FALLBACK_ENDPOINT`(optional): A fallback
184    /// endpoint
185    ///
186    /// If `TOKKIT_TOKEN_INTROSPECTION_QUERY_PARAMETER` is ommitted the access
187    /// token will be part of the URL.
188    pub fn from_env() -> InitializationResult<Self> {
189        let endpoint = env::var("TOKKIT_TOKEN_INTROSPECTION_ENDPOINT").map_err(|err| {
190            InitializationError(format!("'TOKKIT_TOKEN_INTROSPECTION_ENDPOINT': {}", err))
191        })?;
192        let query_parameter = match env::var("TOKKIT_TOKEN_INTROSPECTION_QUERY_PARAMETER") {
193            Ok(v) => Some(v),
194            Err(env::VarError::NotPresent) => None,
195            Err(err) => {
196                return Err(InitializationError(format!(
197                    "'TOKKIT_TOKEN_INTROSPECTION_QUERY_PARAMETER': {}",
198                    err
199                )));
200            }
201        };
202        let fallback_endpoint = match env::var("TOKKIT_TOKEN_INTROSPECTION_FALLBACK_ENDPOINT") {
203            Ok(v) => Some(v),
204            Err(env::VarError::NotPresent) => None,
205            Err(err) => {
206                return Err(InitializationError(format!(
207                    "'TOKKIT_TOKEN_INTROSPECTION_FALLBACK_ENDPOINT': {}",
208                    err
209                )));
210            }
211        };
212        Ok(TokenInfoServiceClientBuilder {
213            parser: Default::default(),
214            endpoint: Some(endpoint),
215            query_parameter,
216            fallback_endpoint,
217        })
218    }
219}
220
221impl TokenInfoServiceClientBuilder<PlanBTokenInfoParser> {
222    /// Create a new `TokenInfoServiceClient` with prepared settings.
223    ///
224    /// [More information](http://planb.readthedocs.io/en/latest/intro.html#token-info)
225    pub fn plan_b(endpoint: String) -> TokenInfoServiceClientBuilder<PlanBTokenInfoParser> {
226        let mut builder = Self::default();
227        builder.with_parser(PlanBTokenInfoParser);
228        builder.with_endpoint(endpoint);
229        builder.with_query_parameter("access_token");
230        builder
231    }
232
233    /// Create a new `TokenInfoServiceClient` with prepared settings from
234    /// environment variables.
235    ///
236    /// `TOKKIT_TOKEN_INTROSPECTION_ENDPOINT` and
237    /// `TOKKIT_TOKEN_INTROSPECTION_FALLBACK_ENDPOINT` will be used and
238    /// `TOKKIT_TOKEN_INTROSPECTION_QUERY_PARAMETER` will have no effect.
239    ///
240    /// [More information](http://planb.readthedocs.io/en/latest/intro.html#token-info)
241    pub fn plan_b_from_env(
242    ) -> InitializationResult<TokenInfoServiceClientBuilder<PlanBTokenInfoParser>> {
243        let mut builder = Self::from_env()?;
244        builder.with_parser(PlanBTokenInfoParser);
245        builder.with_query_parameter("access_token");
246        Ok(builder)
247    }
248}
249
250impl TokenInfoServiceClientBuilder<GoogleV3TokenInfoParser> {
251    /// Create a new `TokenInfoServiceClient` with prepared settings.
252    ///
253    /// [More information](https://developers.google.
254    /// com/identity/protocols/OAuth2UserAgent#validatetoken)
255    pub fn google_v3() -> TokenInfoServiceClientBuilder<GoogleV3TokenInfoParser> {
256        let mut builder = Self::default();
257        builder.with_parser(GoogleV3TokenInfoParser);
258        builder.with_endpoint("https://www.googleapis.com/oauth2/v3/tokeninfo");
259        builder.with_query_parameter("access_token");
260        builder
261    }
262}
263
264impl TokenInfoServiceClientBuilder<AmazonTokenInfoParser> {
265    /// Create a new `TokenInfoServiceClient` with prepared settings.
266    ///
267    /// [More information](https://images-na.ssl-images-amazon.
268    /// com/images/G/01/lwa/dev/docs/website-developer-guide._TTH_.pdf)
269    pub fn amazon() -> TokenInfoServiceClientBuilder<AmazonTokenInfoParser> {
270        let mut builder = Self::default();
271        builder.with_parser(AmazonTokenInfoParser);
272        builder.with_endpoint("https://api.amazon.com/auth/O2/tokeninfo");
273        builder.with_query_parameter("access_token");
274        builder
275    }
276}
277
278impl<P: TokenInfoParser> Default for TokenInfoServiceClientBuilder<P> {
279    fn default() -> Self {
280        TokenInfoServiceClientBuilder {
281            parser: Default::default(),
282            endpoint: Default::default(),
283            query_parameter: Default::default(),
284            fallback_endpoint: Default::default(),
285        }
286    }
287}
288
289/// Introspects an `AccessToken` remotely.
290///
291/// Returns the result as a `TokenInfo`.
292///
293/// The `TokenInfoServiceClient` will do retries on failures and if possible
294/// call a fallback.
295pub struct TokenInfoServiceClient {
296    url_prefix: Arc<String>,
297    fallback_url_prefix: Option<Arc<String>>,
298    http_client: Client,
299    parser: Arc<dyn TokenInfoParser + Sync + Send + 'static>,
300}
301
302impl TokenInfoServiceClient {
303    /// Creates a new `TokenInfoServiceClient`. Fails if one of the given
304    /// endpoints is invalid.
305    pub fn new<P>(
306        endpoint: &str,
307        query_parameter: Option<&str>,
308        fallback_endpoint: Option<&str>,
309        parser: P,
310    ) -> InitializationResult<TokenInfoServiceClient>
311    where
312        P: TokenInfoParser + Sync + Send + 'static,
313    {
314        let url_prefix = assemble_url_prefix(endpoint, &query_parameter)
315            .map_err(InitializationError)?;
316
317        let fallback_url_prefix = if let Some(fallback_endpoint_address) = fallback_endpoint {
318            Some(
319                assemble_url_prefix(fallback_endpoint_address, &query_parameter)
320                    .map_err(InitializationError)?,
321            )
322        } else {
323            None
324        };
325
326        let client = Client::new();
327        Ok(TokenInfoServiceClient {
328            url_prefix: Arc::new(url_prefix),
329            fallback_url_prefix: fallback_url_prefix.map(Arc::new),
330            http_client: client,
331            parser: Arc::new(parser),
332        })
333    }
334}
335
336pub(crate) fn assemble_url_prefix(
337    endpoint: &str,
338    query_parameter: &Option<&str>,
339) -> ::std::result::Result<String, String> {
340    let mut url_prefix = String::from(endpoint);
341
342    if let Some(query_parameter) = query_parameter {
343        if url_prefix.ends_with('/') {
344            url_prefix.pop();
345        }
346        url_prefix.push_str(&format!("?{}=", query_parameter));
347    } else if !url_prefix.ends_with('/') {
348        url_prefix.push('/');
349    }
350
351    let test_url = format!("{}test_token", url_prefix);
352    let _ = test_url
353        .parse::<Url>()
354        .map_err(|err| format!("Invalid URL: {}", err))?;
355
356    Ok(url_prefix)
357}
358
359impl TokenInfoService for TokenInfoServiceClient {
360    fn introspect(&self, token: &AccessToken) -> TokenInfoResult<TokenInfo> {
361        let url: Url = complete_url(&self.url_prefix, token)?;
362        let fallback_url = match self.fallback_url_prefix {
363            Some(ref fb_url_prefix) => Some(complete_url(fb_url_prefix, token)?),
364            None => None,
365        };
366        get_with_fallback(url, fallback_url, &self.http_client, &*self.parser)
367    }
368}
369
370impl Clone for TokenInfoServiceClient {
371    fn clone(&self) -> Self {
372        TokenInfoServiceClient {
373            url_prefix: self.url_prefix.clone(),
374            fallback_url_prefix: self.fallback_url_prefix.clone(),
375            http_client: self.http_client.clone(),
376            parser: self.parser.clone(),
377        }
378    }
379}
380
381fn complete_url(url_prefix: &str, token: &AccessToken) -> TokenInfoResult<Url> {
382    let mut url_str = url_prefix.to_string();
383    url_str.push_str(token.0.as_ref());
384    let url = url_str.parse()?;
385    Ok(url)
386}
387
388fn get_with_fallback(
389    url: Url,
390    fallback_url: Option<Url>,
391    client: &Client,
392    parser: &dyn TokenInfoParser,
393) -> TokenInfoResult<TokenInfo> {
394    get_from_remote(url, client, parser).or_else(|err| match *err.kind() {
395        TokenInfoErrorKind::Client(_) => Err(err),
396        _ => fallback_url
397            .map(|url| get_from_remote(url, client, parser))
398            .unwrap_or(Err(err)),
399    })
400}
401
402fn get_from_remote<P>(
403    url: Url,
404    http_client: &Client,
405    parser: &P,
406) -> TokenInfoResult<TokenInfo>
407where
408    P: TokenInfoParser + ?Sized,
409{
410    let mut op = || match get_from_remote_no_retry(url.clone(), http_client, parser) {
411        Ok(token_info) => Ok(token_info),
412        Err(err) => match *err.kind() {
413            TokenInfoErrorKind::InvalidResponseContent(_) => Err(BackoffError::Permanent(err)),
414            TokenInfoErrorKind::UrlError(_) => Err(BackoffError::Permanent(err)),
415            TokenInfoErrorKind::NotAuthenticated(_) => Err(BackoffError::Permanent(err)),
416            TokenInfoErrorKind::Client(_) => Err(BackoffError::Permanent(err)),
417            _ => Err(BackoffError::Transient(err)),
418        },
419    };
420
421    let mut backoff = ExponentialBackoff::default();
422    backoff.max_elapsed_time = Some(Duration::from_millis(200));
423    backoff.initial_interval = Duration::from_millis(10);
424    backoff.multiplier = 1.5;
425
426    let notify = |err, _| {
427        warn!("Retry on token info service: {}", err);
428    };
429
430    let retry_result = op.retry_notify(&mut backoff, notify);
431
432    match retry_result {
433        Ok(token_info) => Ok(token_info),
434        Err(BackoffError::Transient(err)) => Err(err),
435        Err(BackoffError::Permanent(err)) => Err(err),
436    }
437}
438
439fn get_from_remote_no_retry<P>(
440    url: Url,
441    http_client: &Client,
442    parser: &P,
443) -> TokenInfoResult<TokenInfo>
444where
445    P: TokenInfoParser + ?Sized,
446{
447    let request_builder = http_client.get(url);
448    match request_builder.send() {
449        Ok(ref mut response) => process_response(response, parser),
450        Err(err) => Err(TokenInfoErrorKind::Connection(err.to_string()).into()),
451    }
452}
453
454fn process_response<P>(
455    response: &mut Response,
456    parser: &P,
457) -> TokenInfoResult<TokenInfo>
458where
459    P: TokenInfoParser + ?Sized,
460{
461    let mut body = Vec::new();
462    response
463        .read_to_end(&mut body)
464        .context(TokenInfoErrorKind::Io(
465            "Could not read response bode".to_string(),
466        ))?;
467    if response.status() == StatusCode::OK {
468        let result: TokenInfo = match parser.parse(&body) {
469            Ok(info) => info,
470            Err(msg) => {
471                return Err(TokenInfoErrorKind::InvalidResponseContent(msg.to_string()).into());
472            }
473        };
474        Ok(result)
475    } else if response.status() == StatusCode::UNAUTHORIZED {
476        let msg = str::from_utf8(&body)?;
477        Err(TokenInfoErrorKind::NotAuthenticated(format!(
478            "The server refused the token: {}",
479            msg
480        ))
481        .into())
482    } else if response.status().is_client_error() {
483        let msg = str::from_utf8(&body)?;
484        Err(TokenInfoErrorKind::Client(msg.to_string()).into())
485    } else if response.status().is_server_error() {
486        let msg = str::from_utf8(&body)?;
487        Err(TokenInfoErrorKind::Server(msg.to_string()).into())
488    } else {
489        let msg = str::from_utf8(&body)?;
490        Err(TokenInfoErrorKind::Other(msg.to_string()).into())
491    }
492}
493
494impl From<ParseError> for TokenInfoError {
495    fn from(what: ParseError) -> Self {
496        TokenInfoErrorKind::UrlError(what.to_string()).into()
497    }
498}
499
500impl From<str::Utf8Error> for TokenInfoError {
501    fn from(what: str::Utf8Error) -> Self {
502        TokenInfoErrorKind::InvalidResponseContent(what.to_string()).into()
503    }
504}