twitch_oauth2/tokens/
user_token.rs

1use twitch_types::{UserId, UserIdRef, UserName, UserNameRef};
2
3#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
4use std::time::Instant;
5
6#[cfg(all(target_family = "wasm", target_os = "unknown"))]
7use web_time::Instant;
8
9#[cfg(feature = "client")]
10use super::errors::{
11    DeviceUserTokenExchangeError, ImplicitUserTokenExchangeError, RefreshTokenError,
12    RetrieveTokenError, UserTokenExchangeError,
13};
14#[cfg(feature = "client")]
15use crate::client::Client;
16use crate::{
17    tokens::{
18        errors::{CreationError, ValidationError},
19        Scope, TwitchToken,
20    },
21    types::{AccessToken, ClientId, RefreshToken},
22    ClientSecret, ValidatedToken,
23};
24
25#[allow(clippy::too_long_first_doc_paragraph)] // clippy bug - https://github.com/rust-lang/rust-clippy/issues/13315
26/// An User Token from the [OAuth implicit code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#implicit-grant-flow) or [OAuth authorization code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#authorization-code-grant-flow)
27///
28/// Used for requests that need an authenticated user. See also [`AppAccessToken`](super::AppAccessToken)
29///
30/// See [`UserToken::builder`](UserTokenBuilder::new) for authenticating the user using the `OAuth authorization code flow`.
31#[derive(Clone)]
32pub struct UserToken {
33    /// The access token used to authenticate requests with
34    pub access_token: AccessToken,
35    client_id: ClientId,
36    client_secret: Option<ClientSecret>,
37    /// Username of user associated with this token
38    pub login: UserName,
39    /// User ID of the user associated with this token
40    pub user_id: UserId,
41    /// The refresh token used to extend the life of this user token
42    pub refresh_token: Option<RefreshToken>,
43    /// Expiration from when the response was generated.
44    expires_in: std::time::Duration,
45    /// When this struct was created, not when token was created.
46    struct_created: Instant,
47    scopes: Vec<Scope>,
48    /// Token will never expire
49    ///
50    /// This is only true for old client IDs, like <https://twitchapps.com/tmi> and others
51    pub never_expiring: bool,
52}
53
54impl std::fmt::Debug for UserToken {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        f.debug_struct("UserToken")
57            .field("access_token", &self.access_token)
58            .field("client_id", &self.client_id)
59            .field("client_secret", &self.client_secret)
60            .field("login", &self.login)
61            .field("user_id", &self.user_id)
62            .field("refresh_token", &self.refresh_token)
63            .field("expires_in", &self.expires_in())
64            .field("scopes", &self.scopes)
65            .finish()
66    }
67}
68
69impl UserToken {
70    /// Create a new token
71    ///
72    /// See [`UserToken::from_token`] and [`UserToken::from_existing`] for more ways to create a [`UserToken`]
73    pub fn new(
74        access_token: AccessToken,
75        refresh_token: Option<RefreshToken>,
76        validated: ValidatedToken,
77        client_secret: impl Into<Option<ClientSecret>>,
78    ) -> Result<UserToken, CreationError<std::convert::Infallible>> {
79        let Some(login) = validated.login else {
80            return Err(CreationError::from((
81                access_token,
82                refresh_token,
83                ValidationError::InvalidToken(
84                    "validation did not include a `login`, token might be an app access token",
85                ),
86            )));
87        };
88        let Some(user_id) = validated.user_id else {
89            return Err(CreationError::from((
90                access_token,
91                refresh_token,
92                ValidationError::InvalidToken(
93                    "validation did not include a `user_id`, token might be an app access token",
94                ),
95            )));
96        };
97
98        Ok(UserToken::from_existing_unchecked(
99            access_token,
100            refresh_token,
101            validated.client_id,
102            client_secret,
103            login,
104            user_id,
105            validated.scopes,
106            validated.expires_in,
107        ))
108    }
109
110    /// Create a [UserToken] from an existing active user token. Retrieves [`login`](TwitchToken::login), [`client_id`](TwitchToken::client_id) and [`scopes`](TwitchToken::scopes)
111    ///
112    /// If the token is already expired, this function will fail to produce a [`UserToken`] and return [`ValidationError::NotAuthorized`]
113    ///
114    /// # Examples
115    ///
116    /// ```rust,no_run
117    /// use twitch_oauth2::{AccessToken, UserToken};
118    /// // Make sure you enable the feature "reqwest" for twitch_oauth2 if you want to use reqwest
119    /// # async {let client = twitch_oauth2::client::DummyClient; stringify!(
120    /// let client = reqwest::Client::builder()
121    ///     .redirect(reqwest::redirect::Policy::none())
122    ///     .build()?;
123    /// # );
124    /// let token = UserToken::from_token(&client, AccessToken::from("my_access_token")).await?;
125    /// # Ok::<(), Box<dyn std::error::Error>>(())};
126    /// ```
127    #[cfg(feature = "client")]
128    pub async fn from_token<C>(
129        http_client: &C,
130        access_token: AccessToken,
131    ) -> Result<UserToken, CreationError<<C as Client>::Error>>
132    where
133        C: Client,
134    {
135        Self::from_existing(http_client, access_token, None, None).await
136    }
137
138    /// Creates a [UserToken] using a refresh token. Retrieves the [`login`](TwitchToken::login) and [`scopes`](TwitchToken::scopes).
139    ///
140    /// If an active user token is associated with the provided refresh token, this function will invalidate that existing user token.
141    #[cfg(feature = "client")]
142    pub async fn from_refresh_token<C>(
143        http_client: &C,
144        refresh_token: RefreshToken,
145        client_id: ClientId,
146        client_secret: impl Into<Option<ClientSecret>>,
147    ) -> Result<UserToken, RetrieveTokenError<<C as Client>::Error>>
148    where
149        C: Client,
150    {
151        let client_secret: Option<ClientSecret> = client_secret.into();
152        let (access_token, _, refresh_token) = refresh_token
153            .refresh_token(http_client, &client_id, client_secret.as_ref())
154            .await
155            .map_err(|error| RetrieveTokenError::RefreshTokenError {
156                error,
157                refresh_token,
158            })?;
159        Ok(Self::from_existing(http_client, access_token, refresh_token, client_secret).await?)
160    }
161
162    /// Create a [UserToken] from an existing active user token. Retrieves [`login`](TwitchToken::login) and [`scopes`](TwitchToken::scopes)
163    ///
164    /// If the token is already expired, this function will fail to produce a [`UserToken`] and return [`ValidationError::NotAuthorized`].
165    /// If you have a refresh token, you can use [`UserToken::from_refresh_token`] to refresh the token if was expired.
166    ///
167    /// Consider using [`UserToken::from_existing_or_refresh_token`] to automatically refresh the token if it is expired.
168    ///
169    /// # Examples
170    ///
171    /// ```rust,no_run
172    /// use twitch_oauth2::{AccessToken, ClientSecret, RefreshToken, UserToken};
173    /// // Make sure you enable the feature "reqwest" for twitch_oauth2 if you want to use reqwest
174    /// # async {let client = twitch_oauth2::client::DummyClient; stringify!(
175    /// let client = reqwest::Client::builder()
176    ///     .redirect(reqwest::redirect::Policy::none())
177    ///     .build()?;
178    /// # );
179    /// let token = UserToken::from_existing(
180    ///     &client,
181    ///     AccessToken::from("my_access_token"),
182    ///     RefreshToken::from("my_refresh_token"),
183    ///     ClientSecret::from("my_client_secret"),
184    /// )
185    /// .await?;
186    /// # Ok::<(), Box<dyn std::error::Error>>(())};
187    /// ```
188    #[cfg(feature = "client")]
189    pub async fn from_existing<C>(
190        http_client: &C,
191        access_token: AccessToken,
192        refresh_token: impl Into<Option<RefreshToken>>,
193        client_secret: impl Into<Option<ClientSecret>>,
194    ) -> Result<UserToken, CreationError<<C as Client>::Error>>
195    where
196        C: Client,
197    {
198        let validation_result = access_token.validate_token(http_client).await;
199        let validated = match validation_result {
200            Ok(validated) => validated,
201            Err(e) => return Err(CreationError::from((access_token, refresh_token.into(), e))),
202        };
203        Self::new(access_token, refresh_token.into(), validated, client_secret)
204            .map_err(CreationError::into_other)
205    }
206
207    /// Create a [UserToken] from an existing active user token or refresh token if the access token is expired. Retrieves [`login`](TwitchToken::login), [`client_id`](TwitchToken::client_id) and [`scopes`](TwitchToken::scopes).
208    ///
209    /// # Examples
210    ///
211    /// ```rust,no_run
212    /// use twitch_oauth2::{AccessToken, ClientId, ClientSecret, RefreshToken, UserToken};
213    /// // Make sure you enable the feature "reqwest" for twitch_oauth2 if you want to use reqwest
214    /// # async {let client = twitch_oauth2::client::DummyClient; stringify!(
215    /// let client = reqwest::Client::builder()
216    ///    .redirect(reqwest::redirect::Policy::none())
217    ///    .build()?;
218    /// # );
219    /// let token = UserToken::from_existing_or_refresh_token(
220    ///     &client,
221    ///     AccessToken::from("my_access_token"),
222    ///     RefreshToken::from("my_refresh_token"),
223    ///     ClientId::from("my_client_id"),
224    ///     ClientSecret::from("my_optional_client_secret"),
225    /// ).await?;
226    /// # Ok::<(), Box<dyn std::error::Error>>(())};
227    #[cfg(feature = "client")]
228    pub async fn from_existing_or_refresh_token<C>(
229        http_client: &C,
230        access_token: AccessToken,
231        refresh_token: RefreshToken,
232        client_id: ClientId,
233        client_secret: impl Into<Option<ClientSecret>>,
234    ) -> Result<UserToken, RetrieveTokenError<<C as Client>::Error>>
235    where
236        C: Client,
237    {
238        match access_token.validate_token(http_client).await {
239            Ok(v) => Self::new(access_token, Some(refresh_token), v, client_secret)
240                .map_err(|error| error.into_other().into()),
241            Err(ValidationError::NotAuthorized) => {
242                Self::from_refresh_token(http_client, refresh_token, client_id, client_secret).await
243            }
244            Err(error) => Err(RetrieveTokenError::ValidationError {
245                error,
246                access_token,
247                refresh_token: Some(refresh_token),
248            }),
249        }
250    }
251
252    /// Assemble token without checks.
253    ///
254    /// # Notes
255    ///
256    /// If `expires_in` is `None`, we'll assume [`token.is_elapsed`](TwitchToken::is_elapsed) is always false
257    #[allow(clippy::too_many_arguments)]
258    pub fn from_existing_unchecked(
259        access_token: impl Into<AccessToken>,
260        refresh_token: impl Into<Option<RefreshToken>>,
261        client_id: impl Into<ClientId>,
262        client_secret: impl Into<Option<ClientSecret>>,
263        login: UserName,
264        user_id: UserId,
265        scopes: Option<Vec<Scope>>,
266        expires_in: Option<std::time::Duration>,
267    ) -> UserToken {
268        UserToken {
269            access_token: access_token.into(),
270            client_id: client_id.into(),
271            client_secret: client_secret.into(),
272            login,
273            user_id,
274            refresh_token: refresh_token.into(),
275            expires_in: expires_in.unwrap_or(std::time::Duration::MAX),
276            struct_created: Instant::now(),
277            scopes: scopes.unwrap_or_default(),
278            never_expiring: expires_in.is_none(),
279        }
280    }
281
282    /// Assemble token from twitch responses.
283    pub fn from_response(
284        response: crate::id::TwitchTokenResponse,
285        validated: ValidatedToken,
286        client_secret: impl Into<Option<ClientSecret>>,
287    ) -> Result<UserToken, CreationError<std::convert::Infallible>> {
288        Self::new(
289            response.access_token,
290            response.refresh_token,
291            validated,
292            client_secret,
293        )
294    }
295
296    #[doc(hidden)]
297    /// Returns true if this token is never expiring.
298    ///
299    /// Hidden because it's not expected to be used.
300    pub fn never_expires(&self) -> bool { self.never_expiring }
301
302    /// Create a [`UserTokenBuilder`] to get a token with the [OAuth Authorization Code](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#authorization-code-grant-flow)
303    pub fn builder(
304        client_id: ClientId,
305        client_secret: ClientSecret,
306        // FIXME: Braid or string or this?
307        redirect_url: url::Url,
308    ) -> UserTokenBuilder {
309        UserTokenBuilder::new(client_id, client_secret, redirect_url)
310    }
311
312    /// Generate a user token from [mock-api](https://github.com/twitchdev/twitch-cli/blob/main/docs/mock-api.md#auth-namespace)
313    ///
314    /// # Examples
315    ///
316    /// ```rust,no_run
317    /// # #[tokio::main]
318    /// # async fn run() -> Result<(), Box<dyn std::error::Error + 'static>>{
319    /// let token = twitch_oauth2::UserToken::mock_token(
320    ///     &reqwest::Client::builder()
321    ///         .redirect(reqwest::redirect::Policy::none())
322    ///         .build()?,
323    ///     "mockclientid".into(),
324    ///     "mockclientsecret".into(),
325    ///     "user_id",
326    ///     vec![],
327    /// )
328    /// .await?;
329    /// # Ok(())}
330    /// # fn main() {run();}
331    /// ```
332    #[cfg(all(feature = "mock_api", feature = "client"))]
333    pub async fn mock_token<C>(
334        http_client: &C,
335        client_id: ClientId,
336        client_secret: ClientSecret,
337        user_id: impl AsRef<str>,
338        scopes: Vec<Scope>,
339    ) -> Result<UserToken, UserTokenExchangeError<<C as Client>::Error>>
340    where
341        C: Client,
342    {
343        use http::{HeaderMap, Method};
344        use std::collections::HashMap;
345
346        let user_id = user_id.as_ref();
347        let scope_str = scopes.as_slice().join(" ");
348        let mut params = HashMap::new();
349        params.insert("client_id", client_id.as_str());
350        params.insert("client_secret", client_secret.secret());
351        params.insert("grant_type", "user_token");
352        params.insert("scope", &scope_str);
353        params.insert("user_id", user_id);
354
355        let req = crate::construct_request(
356            &crate::AUTH_URL,
357            &params,
358            HeaderMap::new(),
359            Method::POST,
360            vec![],
361        );
362
363        let resp = http_client
364            .req(req)
365            .await
366            .map_err(UserTokenExchangeError::RequestError)?;
367        let response = crate::id::TwitchTokenResponse::from_response(&resp)?;
368
369        Ok(UserToken::from_existing(
370            http_client,
371            response.access_token,
372            response.refresh_token,
373            client_secret,
374        )
375        .await?)
376    }
377
378    /// Set the client secret
379    pub fn set_secret(&mut self, secret: Option<ClientSecret>) { self.client_secret = secret }
380}
381
382#[cfg_attr(feature = "client", async_trait::async_trait)]
383impl TwitchToken for UserToken {
384    fn token_type() -> super::BearerTokenType { super::BearerTokenType::UserToken }
385
386    fn client_id(&self) -> &ClientId { &self.client_id }
387
388    fn token(&self) -> &AccessToken { &self.access_token }
389
390    fn login(&self) -> Option<&UserNameRef> { Some(&self.login) }
391
392    fn user_id(&self) -> Option<&UserIdRef> { Some(&self.user_id) }
393
394    #[cfg(feature = "client")]
395    async fn refresh_token<'a, C>(
396        &mut self,
397        http_client: &'a C,
398    ) -> Result<(), RefreshTokenError<<C as Client>::Error>>
399    where
400        Self: Sized,
401        C: Client,
402    {
403        let (access_token, expires, refresh_token) = if let Some(token) = self.refresh_token.take()
404        {
405            token
406                .refresh_token(http_client, &self.client_id, self.client_secret.as_ref())
407                .await?
408        } else {
409            return Err(RefreshTokenError::NoRefreshToken);
410        };
411        self.access_token = access_token;
412        self.expires_in = expires;
413        self.refresh_token = refresh_token;
414        self.struct_created = Instant::now();
415        Ok(())
416    }
417
418    fn expires_in(&self) -> std::time::Duration {
419        if !self.never_expiring {
420            self.expires_in
421                .checked_sub(self.struct_created.elapsed())
422                .unwrap_or_default()
423        } else {
424            // We don't return an option here because it's not expected to use this if the token is known to be unexpiring.
425            std::time::Duration::MAX
426        }
427    }
428
429    fn scopes(&self) -> &[Scope] { self.scopes.as_slice() }
430}
431
432/// Builder for [OAuth authorization code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#authorization-code-grant-flow)
433///
434/// See [`ImplicitUserTokenBuilder`] for the [OAuth implicit code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#implicit-grant-flow) (does not require Client Secret)
435///
436/// # Examples
437///
438/// See also [the auth flow example](https://github.com/twitch-rs/twitch_oauth2/blob/main/examples/auth_flow.rs)
439///
440/// To generate a user token with this auth flow, you need to:
441///
442/// 1. Initialize the [`UserTokenBuilder`] with [`UserTokenBuilder::new`](UserTokenBuilder::new), providing your client id, client secret, and a redirect URL.
443///    Use [`set_scopes(vec![])`](UserTokenBuilder::set_scopes) to add any necessary scopes to the request. You can also use [`force_verify(true)`](UserTokenBuilder::force_verify) to force the user to
444///    re-authorize your app’s access to their resources.
445///
446///     Make sure you've added the redirect URL to the app settings on [the Twitch Developer Console](https://dev.twitch.tv/console).
447///
448///     ```rust
449///     use twitch_oauth2::{id::TwitchTokenResponse, tokens::UserTokenBuilder, Scope};
450///     use url::Url;
451///
452///     // This is the URL the user will be redirected to after authorizing your application
453///     let redirect_url = Url::parse("http://localhost/twitch/register")?;
454///     let mut builder = UserTokenBuilder::new("myclientid", "myclientsecret", redirect_url);
455///     builder = builder.set_scopes(vec![Scope::ChatRead, Scope::ChatEdit]);
456///     builder = builder.force_verify(true); // Defaults to false
457///     # Ok::<(), Box<dyn std::error::Error>>(())
458///     ```
459///
460/// 2. Generate a URL for the user to visit using [`generate_url()`](UserTokenBuilder::generate_url). This method also returns a CSRF token that you need to save for later validation.
461///
462///     ```rust
463///     # use twitch_oauth2::{id::TwitchTokenResponse, tokens::UserTokenBuilder, Scope};
464///     # use url::Url;
465///     # let redirect_url = Url::parse("http://localhost/twitch/register")?;
466///     # let mut builder = UserTokenBuilder::new("myclientid", "myclientsecret", redirect_url);
467///     let (url, csrf_token) = builder.generate_url();
468///     // Make your user navigate to this URL, for example
469///     println!("Visit this URL to authorize Twitch access: {}", url);
470///     # Ok::<(), Box<dyn std::error::Error>>(())
471///     ```
472///
473/// 3. Have the user visit the generated URL. They will be asked to authorize your application if they haven't previously done so
474///    or if you've set [`force_verify`](UserTokenBuilder::force_verify) to `true`.
475///
476///    You can do this by providing the link in [a web page](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a), have the user [be directed](https://developer.mozilla.org/en-US/docs/Web/API/Location/assign),
477///    the console, or by [opening it](https://docs.rs/webbrowser/0.8.10/webbrowser/) in a browser.
478///
479///    If this is a web server, you should store the [UserTokenBuilder] somewhere you can retrieve it later. A good place to store it is in a [`Cache`](https://docs.rs/retainer/0.3.0/retainer/cache/struct.Cache.html)
480///    or a [`HashMap`](std::collections::HashMap) with the CSRF token as the key.
481///
482/// 4. When the user has been redirected to the redirect URL by twitch, extract the `state` and `code` query parameters from the URL.
483///
484///     ```rust
485///     use std::borrow::Cow;
486///     use std::collections::BTreeMap;
487///
488///     fn extract_pair<'a>(
489///         query: &BTreeMap<Cow<'a, str>, Cow<'a, str>>,
490///         key1: &str,
491///         key2: &str,
492///     ) -> Option<(Cow<'a, str>, Cow<'a, str>)> {
493///         Some((query.get(key1)?.clone(), query.get(key2)?.clone()))
494///     }
495///
496///     /// Extract the state and code from the URL a user was redirected to after authorizing the application.
497///     fn extract_url<'a>(
498///         url: &'a url::Url,
499///     ) -> Result<(Cow<'a, str>, Cow<'a, str>), Option<(Cow<'a, str>, Cow<'a, str>)>> {
500///         let query: BTreeMap<_, _> = url.query_pairs().collect();
501///         if let Some((error, error_description)) = extract_pair(&query, "error", "error_description") {
502///             Err(Some((error, error_description)))
503///         } else if let Some((state, code)) = extract_pair(&query, "state", "code") {
504///             Ok((state, code))
505///         } else {
506///             Err(None)
507///         }
508///     }
509///     ```
510/// 5. Finally, call [`get_user_token`](UserTokenBuilder::get_user_token) with the `state` and `code` query parameters to get the user's access token.
511///
512///     ```rust
513///     # async move {
514///     # use twitch_oauth2::{id::TwitchTokenResponse, tokens::UserTokenBuilder, Scope};
515///     # use url::Url;
516///     # use std::borrow::Cow;
517///     # let redirect_url = Url::parse("http://localhost/twitch/register")?;
518///     # let mut builder = UserTokenBuilder::new("myclientid", "myclientsecret", redirect_url);
519///     # let (url, csrf_token) = builder.generate_url();
520///     # fn extract_url<'a>(_: &'a url::Url) -> Result<(Cow<'a, str>, Cow<'a, str>), std::io::Error> { Ok((Cow::default(), Cow::default())) }
521///     # let url = url::Url::parse("http://localhost/twitch/register?code=code&state=state")?;
522///     # let client = twitch_oauth2::client::DummyClient; stringify!(
523///     let client = reqwest::Client::builder()
524///         .redirect(reqwest::redirect::Policy::none())
525///         .build()?;
526///     # );
527///     let (state, code) = extract_url(&url)?;
528///     let token = builder.get_user_token(&client, state.as_ref(), code.as_ref()).await?;
529///     println!("User token: {:?}", token);
530///     # Ok::<(), Box<dyn std::error::Error>>(())
531///     # };
532///     ```
533pub struct UserTokenBuilder {
534    pub(crate) scopes: Vec<Scope>,
535    pub(crate) csrf: Option<crate::types::CsrfToken>,
536    pub(crate) force_verify: bool,
537    pub(crate) redirect_url: url::Url,
538    client_id: ClientId,
539    client_secret: ClientSecret,
540}
541
542impl UserTokenBuilder {
543    /// Create a [`UserTokenBuilder`]
544    ///
545    /// # Notes
546    ///
547    /// The `redirect_url` must be present, verbatim, on [the Twitch Developer Console](https://dev.twitch.tv/console).
548    ///
549    /// The `url` crate converts empty paths into "/" (such as `https://example.com` into `https://example.com/`),
550    /// which means that you'll need to add `https://example.com/` to your redirect URIs (note the "trailing" slash) if you want to use an empty path.
551    ///
552    /// To avoid this, use a path such as `https://example.com/twitch/register` or similar instead, where the `url` crate would not add a trailing `/`.
553    pub fn new(
554        client_id: impl Into<ClientId>,
555        client_secret: impl Into<ClientSecret>,
556        redirect_url: url::Url,
557    ) -> UserTokenBuilder {
558        UserTokenBuilder {
559            scopes: vec![],
560            csrf: None,
561            force_verify: false,
562            redirect_url,
563            client_id: client_id.into(),
564            client_secret: client_secret.into(),
565        }
566    }
567
568    /// Add scopes to the request
569    pub fn set_scopes(mut self, scopes: Vec<Scope>) -> Self {
570        self.scopes = scopes;
571        self
572    }
573
574    /// Add a single scope to request
575    pub fn add_scope(&mut self, scope: Scope) { self.scopes.push(scope); }
576
577    /// Enable or disable function to make the user able to switch accounts if needed.
578    pub fn force_verify(mut self, b: bool) -> Self {
579        self.force_verify = b;
580        self
581    }
582
583    /// Generate the URL to request a code.
584    ///
585    /// First step in the [guide](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#get-the-user-to-authorize-your-app)
586    pub fn generate_url(&mut self) -> (url::Url, crate::types::CsrfToken) {
587        let csrf = crate::types::CsrfToken::new_random();
588        self.csrf = Some(csrf.clone());
589        let mut url = crate::AUTH_URL.clone();
590
591        let auth = vec![
592            ("response_type", "code"),
593            ("client_id", self.client_id.as_str()),
594            ("redirect_uri", self.redirect_url.as_str()),
595            ("state", csrf.as_str()),
596        ];
597
598        url.query_pairs_mut().extend_pairs(auth);
599
600        if !self.scopes.is_empty() {
601            url.query_pairs_mut()
602                .append_pair("scope", &self.scopes.as_slice().join(" "));
603        }
604
605        if self.force_verify {
606            url.query_pairs_mut().append_pair("force_verify", "true");
607        };
608
609        (url, csrf)
610    }
611
612    /// Set the CSRF token.
613    ///
614    /// Hidden because you should preferably not use this.
615    #[doc(hidden)]
616    pub fn set_csrf(&mut self, csrf: crate::types::CsrfToken) { self.csrf = Some(csrf); }
617
618    /// Check if the CSRF is valid
619    pub fn csrf_is_valid(&self, csrf: &str) -> bool {
620        if let Some(csrf2) = &self.csrf {
621            csrf2.secret() == csrf
622        } else {
623            false
624        }
625    }
626
627    /// Get the request for getting a [TwitchTokenResponse](crate::id::TwitchTokenResponse), to be used in [UserToken::from_response].
628    ///
629    /// # Examples
630    ///
631    /// ```rust
632    /// use twitch_oauth2::{tokens::UserTokenBuilder, id::TwitchTokenResponse};
633    /// use url::Url;
634    /// let callback_url = Url::parse("http://localhost/twitch/register")?;
635    /// let mut builder = UserTokenBuilder::new("myclientid", "myclientsecret", callback_url);
636    /// let (url, _csrf_code) = builder.generate_url();
637    ///
638    /// // Direct the user to this url.
639    /// // Later when your server gets a response on `callback_url` with `?code=xxxxxxx&state=xxxxxxx&scope=aa%3Aaa+bb%3Abb`
640    ///
641    /// // validate the state
642    /// # let state_in_query = _csrf_code.secret();
643    /// if !builder.csrf_is_valid(state_in_query) {
644    ///     panic!("state mismatched")
645    /// }
646    /// // and then get your token
647    /// # let code_in_query = _csrf_code.secret();
648    /// let request = builder.get_user_token_request(code_in_query);
649    ///
650    /// // use your favorite http client
651    ///
652    /// let response: http::Response<Vec<u8>> = client_req(request);
653    /// let twitch_response = TwitchTokenResponse::from_response(&response)?;
654    ///
655    /// // you now have a access token, do what you want with it.
656    /// // You're recommended to convert it into a `UserToken` via `UserToken::from_response`
657    ///
658    /// // You can validate the access_token like this
659    /// let validated_req = twitch_response.access_token.validate_token_request();
660    /// # fn client_req(_: http::Request<Vec<u8>>) -> http::Response<Vec<u8>> { http::Response::new(
661    /// # r#"{"access_token":"rfx2uswqe8l4g1mkagrvg5tv0ks3","expires_in":14124,"refresh_token":"5b93chm6hdve3mycz05zfzatkfdenfspp1h1ar2xxdalen01","scope":["channel:moderate","chat:edit","chat:read"],"token_type":"bearer"}"#.bytes().collect()
662    /// # ) }
663    /// # Ok::<(), Box<dyn std::error::Error>>(())
664    /// ```
665    pub fn get_user_token_request(&self, code: &str) -> http::Request<Vec<u8>> {
666        use http::{HeaderMap, Method};
667        use std::collections::HashMap;
668        let mut params = HashMap::new();
669        params.insert("client_id", self.client_id.as_str());
670        params.insert("client_secret", self.client_secret.secret());
671        params.insert("code", code);
672        params.insert("grant_type", "authorization_code");
673        params.insert("redirect_uri", self.redirect_url.as_str());
674
675        crate::construct_request(
676            &crate::TOKEN_URL,
677            &params,
678            HeaderMap::new(),
679            Method::POST,
680            vec![],
681        )
682    }
683
684    /// Generate the code with the help of the authorization code
685    ///
686    /// Last step in the [guide](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#use-the-authorization-code-to-get-a-token)
687    ///
688    /// On failure to authenticate due to wrong redirect url or other errors, twitch redirects the user to `<redirect_url or first defined url in dev console>?error=<error type>&error_description=<description of error>`
689    #[cfg(feature = "client")]
690    pub async fn get_user_token<C>(
691        self,
692        http_client: &C,
693        state: &str,
694        // TODO: Should be either str or AuthorizationCode
695        code: &str,
696    ) -> Result<UserToken, UserTokenExchangeError<<C as Client>::Error>>
697    where
698        C: Client,
699    {
700        if !self.csrf_is_valid(state) {
701            return Err(UserTokenExchangeError::StateMismatch);
702        }
703
704        let req = self.get_user_token_request(code);
705
706        let resp = http_client
707            .req(req)
708            .await
709            .map_err(UserTokenExchangeError::RequestError)?;
710
711        let response = crate::id::TwitchTokenResponse::from_response(&resp)?;
712        let validated = response.access_token.validate_token(http_client).await?;
713
714        UserToken::from_response(response, validated, self.client_secret)
715            .map_err(|e| e.into_other().into())
716    }
717}
718
719/// Builder for [OAuth implicit code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#implicit-grant-flow)
720///
721/// See [`UserTokenBuilder`] for the [OAuth authorization code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#authorization-code-grant-flow) (requires Client Secret, generally more secure)
722pub struct ImplicitUserTokenBuilder {
723    pub(crate) scopes: Vec<Scope>,
724    pub(crate) csrf: Option<crate::types::CsrfToken>,
725    pub(crate) redirect_url: url::Url,
726    pub(crate) force_verify: bool,
727    client_id: ClientId,
728}
729
730impl ImplicitUserTokenBuilder {
731    /// Create a [`ImplicitUserTokenBuilder`]
732    ///
733    /// # Notes
734    ///
735    /// The `redirect_url` must be present, verbatim, on [the Twitch Developer Console](https://dev.twitch.tv/console).
736    ///
737    /// The `url` crate converts empty paths into "/" (such as `https://example.com` into `https://example.com/`),
738    /// which means that you'll need to add `https://example.com/` to your redirect URIs (note the "trailing" slash) if you want to use an empty path.
739    ///
740    /// To avoid this, use a path such as `https://example.com/twitch/register` or similar instead, where the `url` crate would not add a trailing `/`.
741    pub fn new(client_id: ClientId, redirect_url: url::Url) -> ImplicitUserTokenBuilder {
742        ImplicitUserTokenBuilder {
743            scopes: vec![],
744            redirect_url,
745            csrf: None,
746            force_verify: false,
747            client_id,
748        }
749    }
750
751    /// Add scopes to the request
752    pub fn set_scopes(mut self, scopes: Vec<Scope>) -> Self {
753        self.scopes = scopes;
754        self
755    }
756
757    /// Add a single scope to request
758    pub fn add_scope(&mut self, scope: Scope) { self.scopes.push(scope); }
759
760    /// Enable or disable function to make the user able to switch accounts if needed.
761    pub fn force_verify(mut self, b: bool) -> Self {
762        self.force_verify = b;
763        self
764    }
765
766    /// Generate the URL to request a token.
767    ///
768    /// First step in the [guide](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#implicit-grant-flow)
769    pub fn generate_url(&mut self) -> (url::Url, crate::types::CsrfToken) {
770        let csrf = crate::types::CsrfToken::new_random();
771        self.csrf = Some(csrf.clone());
772        let mut url = crate::AUTH_URL.clone();
773
774        let auth = vec![
775            ("response_type", "token"),
776            ("client_id", self.client_id.as_str()),
777            ("redirect_uri", self.redirect_url.as_str()),
778            ("state", csrf.as_str()),
779        ];
780
781        url.query_pairs_mut().extend_pairs(auth);
782
783        if !self.scopes.is_empty() {
784            url.query_pairs_mut()
785                .append_pair("scope", &self.scopes.as_slice().join(" "));
786        }
787
788        if self.force_verify {
789            url.query_pairs_mut().append_pair("force_verify", "true");
790        };
791
792        (url, csrf)
793    }
794
795    /// Check if the CSRF is valid
796    pub fn csrf_is_valid(&self, csrf: &str) -> bool {
797        if let Some(csrf2) = &self.csrf {
798            csrf2.secret() == csrf
799        } else {
800            false
801        }
802    }
803
804    /// Generate the code with the help of the hash.
805    ///
806    /// You can skip this method and instead use the token in the hash directly with [`UserToken::from_existing()`], but it's provided here for convenience.
807    ///
808    /// Last step in the [guide](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#implicit-grant-flow)
809    ///
810    /// # Example
811    ///
812    /// When the user authenticates, they are sent to `<redirecturl>#access_token=<access_token>&scope=<scopes, space (%20) separated>&state=<csrf state>&token_type=bearer`
813    ///
814    /// On failure, they are sent to
815    ///
816    /// `<redirect_url or first defined url in dev console>?error=<error type>&error_description=<error description>&state=<csrf state>`
817    /// Get the hash of the url with javascript.
818    ///
819    /// ```js
820    /// document.location.hash.substr(1);
821    /// ```
822    ///
823    /// and send it to your client in what ever way convenient.
824    ///
825    /// Provided below is an example of how to do it, no guarantees on the safety of this method.
826    ///
827    /// ```html
828    /// <!DOCTYPE html>
829    /// <html>
830    /// <head>
831    /// <title>Authorization</title>
832    /// <meta name="ROBOTS" content="NOFOLLOW">
833    /// <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
834    /// <script type="text/javascript">
835    /// <!--
836    /// function initiate() {
837    ///     var hash = document.location.hash.substr(1);
838    ///     document.getElementById("javascript").className = "";
839    ///     if (hash != null) {
840    ///             document.location.replace("/token?"+hash);
841    ///     }
842    ///     else {
843    ///         document.getElementById("javascript").innerHTML = "Error: Access Token not found";
844    ///     }
845    /// }
846    /// -->
847    /// </script>
848    /// <style type="text/css">
849    ///     body { text-align: center; background-color: #FFF; max-width: 500px; margin: auto; }
850    ///     noscript { color: red;  }
851    ///     .hide { display: none; }
852    /// </style>
853    /// </head>
854    /// <body onload="initiate()">
855    /// <h1>Authorization</h1>
856    /// <noscript>
857    ///     <p>This page requires <strong>JavaScript</strong> to get your token.
858    /// </noscript>
859    /// <p id="javascript" class="hide">
860    /// You should be redirected..
861    /// </p>
862    /// </body>
863    /// </html>
864    /// ```
865    ///
866    /// where `/token?` gives this function it's corresponding arguments in query params
867    ///
868    /// Make sure that `/token` removes the query from the history.
869    ///
870    /// ```html
871    /// <!DOCTYPE html>
872    /// <html>
873    /// <head>
874    /// <title>Authorization Successful</title>
875    /// <meta name="ROBOTS" content="NOFOLLOW">
876    /// <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
877    /// <script type="text/javascript">
878    /// <!--
879    /// function initiate() {
880    ///     //
881    ///     document.location.replace("/token_retrieved);
882    /// }
883    /// -->
884    /// </script>
885    /// <style type="text/css">
886    ///     body { text-align: center; background-color: #FFF; max-width: 500px; margin: auto; }
887    /// </style>
888    /// </head>
889    /// <body onload="initiate()">
890    /// <h1>Authorization Successful</h1>
891    /// </body>
892    /// </html>
893    /// ```
894    #[cfg(feature = "client")]
895    pub async fn get_user_token<C>(
896        self,
897        http_client: &C,
898        state: Option<&str>,
899        access_token: Option<&str>,
900        error: Option<&str>,
901        error_description: Option<&str>,
902    ) -> Result<UserToken, ImplicitUserTokenExchangeError<<C as Client>::Error>>
903    where
904        C: Client,
905    {
906        if !state.map(|s| self.csrf_is_valid(s)).unwrap_or_default() {
907            return Err(ImplicitUserTokenExchangeError::StateMismatch);
908        }
909
910        match (access_token, error, error_description) {
911            (Some(access_token), None, None) => UserToken::from_existing(
912                http_client,
913                crate::types::AccessToken::from(access_token),
914                None,
915                None,
916            )
917            .await
918            .map_err(From::from),
919            (_, error, description) => {
920                let (error, description) = (
921                    error.map(|s| s.to_string()),
922                    description.map(|s| s.to_string()),
923                );
924                Err(ImplicitUserTokenExchangeError::TwitchError { error, description })
925            }
926        }
927    }
928}
929
930/// Builder for [OAuth device code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#device-flow)
931///
932/// # Examples
933///
934/// ```rust
935/// # async move {
936/// # use twitch_oauth2::{id::TwitchTokenResponse, UserToken, tokens::DeviceUserTokenBuilder, Scope};
937/// # use url::Url;
938/// # use std::borrow::Cow;
939/// # let client = twitch_oauth2::client::DummyClient; stringify!(
940/// let client = reqwest::Client::builder()
941///     .redirect(reqwest::redirect::Policy::none())
942///     .build()?;
943/// # );
944/// let mut builder = DeviceUserTokenBuilder::new("myclientid", vec![Scope::ChatRead, Scope::ChatEdit]);
945/// let code = builder.start(&client).await?;
946/// println!("Please go to {}", code.verification_uri);
947/// let token = builder.wait_for_code(&client, tokio::time::sleep).await?;
948/// println!("Token: {:?}", token);
949/// # Ok::<(), Box<dyn std::error::Error>>(())
950/// # };
951/// ```
952pub struct DeviceUserTokenBuilder {
953    client_id: ClientId,
954    client_secret: Option<ClientSecret>,
955    scopes: Vec<Scope>,
956    response: Option<(Instant, crate::id::DeviceCodeResponse)>,
957}
958
959impl DeviceUserTokenBuilder {
960    /// Create a [`DeviceUserTokenBuilder`]
961    pub fn new(client_id: impl Into<ClientId>, scopes: Vec<Scope>) -> Self {
962        Self {
963            client_id: client_id.into(),
964            client_secret: None,
965            scopes,
966            response: None,
967        }
968    }
969
970    /// Set the client secret, only necessary if you have one
971    pub fn set_secret(&mut self, secret: Option<ClientSecret>) { self.client_secret = secret; }
972
973    /// Get the request for getting a [`DeviceCodeResponse`](crate::id::DeviceCodeResponse)
974    pub fn get_exchange_device_code_request(&self) -> http::Request<Vec<u8>> {
975        // the equivalent of curl --location 'https://id.twitch.tv/oauth2/device' \
976        // --form 'client_id="<clientID>"' \
977        // --form 'scopes="<scopes>"'
978        use http::{HeaderMap, Method};
979        use std::collections::HashMap;
980        let mut params = HashMap::new();
981        params.insert("client_id", self.client_id.as_str());
982        let scopes = self.scopes.as_slice().join(" ");
983        if !scopes.is_empty() {
984            params.insert("scopes", &scopes);
985        }
986        crate::construct_request(
987            &crate::DEVICE_URL,
988            params,
989            HeaderMap::new(),
990            Method::POST,
991            vec![],
992        )
993    }
994
995    /// Parse the response from the device code request
996    pub fn parse_exchange_device_code_response(
997        &mut self,
998        response: http::Response<Vec<u8>>,
999    ) -> Result<&crate::id::DeviceCodeResponse, crate::RequestParseError> {
1000        let response = crate::parse_response(&response)?;
1001        self.response = Some((Instant::now(), response));
1002        Ok(&self.response.as_ref().unwrap().1)
1003    }
1004
1005    /// Start the device code flow
1006    ///
1007    /// # Notes
1008    ///
1009    /// Use [`DeviceCodeResponse::verification_uri`](crate::id::DeviceCodeResponse::verification_uri) to get the URL the user needs to visit.
1010    #[cfg(feature = "client")]
1011    pub async fn start<'s, C>(
1012        &'s mut self,
1013        http_client: &C,
1014    ) -> Result<&'s crate::id::DeviceCodeResponse, DeviceUserTokenExchangeError<C::Error>>
1015    where
1016        C: Client,
1017    {
1018        let req = self.get_exchange_device_code_request();
1019        let resp = http_client
1020            .req(req)
1021            .await
1022            .map_err(DeviceUserTokenExchangeError::DeviceExchangeRequestError)?;
1023        self.parse_exchange_device_code_response(resp)
1024            .map_err(DeviceUserTokenExchangeError::DeviceExchangeParseError)
1025    }
1026
1027    /// Get the request for getting a [`TwitchTokenResponse`](crate::id::TwitchTokenResponse), to be used in [UserToken::from_response].
1028    ///
1029    /// Returns None if there is no `device_code`
1030    pub fn get_user_token_request(&self) -> Option<http::Request<Vec<u8>>> {
1031        use http::{HeaderMap, Method};
1032        use std::collections::HashMap;
1033        let Some((_, response)) = &self.response else {
1034            return None;
1035        };
1036        let mut params = HashMap::new();
1037        params.insert("client_id", self.client_id.as_str());
1038        params.insert("device_code", &response.device_code);
1039        params.insert("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
1040
1041        Some(crate::construct_request(
1042            &crate::TOKEN_URL,
1043            &params,
1044            HeaderMap::new(),
1045            Method::POST,
1046            vec![],
1047        ))
1048    }
1049
1050    /// Finish the device code flow by waiting for the user to authorize, granting you a token if the user has authorized the app.
1051    ///
1052    /// Will return [`DeviceUserTokenExchangeError::Expired`] if the user has not authorized the app within the [`expires_in`](crate::id::DeviceCodeResponse::expires_in) time.
1053    ///
1054    /// # Examples
1055    ///
1056    /// ```rust
1057    /// # async move {
1058    /// # use twitch_oauth2::{id::TwitchTokenResponse, UserToken, tokens::DeviceUserTokenBuilder, Scope};
1059    /// # use url::Url;
1060    /// # use std::borrow::Cow;
1061    /// # let client = twitch_oauth2::client::DummyClient; stringify!(
1062    /// let client = reqwest::Client::builder()
1063    ///     .redirect(reqwest::redirect::Policy::none())
1064    ///     .build()?;
1065    /// # );
1066    /// let mut builder = DeviceUserTokenBuilder::new("myclientid", vec![Scope::ChatRead, Scope::ChatEdit]);
1067    /// let code = builder.start(&client).await?;
1068    /// println!("Please go to {}", code.verification_uri);
1069    /// let token = builder.wait_for_code(&client, tokio::time::sleep).await?;
1070    /// println!("Token: {:?}", token);
1071    /// # Ok::<(), Box<dyn std::error::Error>>(())
1072    /// # };
1073    /// ```
1074    #[cfg(feature = "client")]
1075    pub async fn wait_for_code<C, Fut>(
1076        &mut self,
1077        client: &C,
1078        wait_fn: impl Fn(std::time::Duration) -> Fut,
1079    ) -> Result<UserToken, DeviceUserTokenExchangeError<C::Error>>
1080    where
1081        C: Client,
1082        Fut: std::future::Future<Output = ()>,
1083    {
1084        let (created, response) = self
1085            .response
1086            .as_ref()
1087            .ok_or(DeviceUserTokenExchangeError::NoDeviceCode)?;
1088        let mut finish = self.try_finish(client).await;
1089        while finish.as_ref().is_err_and(|e| e.is_pending()) {
1090            wait_fn(std::time::Duration::from_secs(response.interval)).await;
1091            finish = self.try_finish(client).await;
1092            if created.elapsed() > std::time::Duration::from_secs(response.expires_in) {
1093                return Err(DeviceUserTokenExchangeError::Expired);
1094            }
1095        }
1096
1097        let token = finish?;
1098        Ok(token)
1099    }
1100
1101    /// Finish the device code flow, granting you a token if the user has authorized the app.
1102    /// Consider using the [`wait_for_code`](Self::wait_for_code) method instead.
1103    ///
1104    /// # Notes
1105    ///
1106    /// Must be called after [`start`](Self::start) and will return an error if not.
1107    /// The error could be that the user has not authorized the app yet, in which case you should wait for [`interval`](crate::id::DeviceCodeResponse::interval) seconds and try again.
1108    /// To check for this condition, use the [`is_pending`](DeviceUserTokenExchangeError::is_pending) method.
1109    ///
1110    /// # Examples
1111    ///
1112    /// ```rust
1113    /// # async move {
1114    /// # use twitch_oauth2::{id::TwitchTokenResponse, UserToken, tokens::DeviceUserTokenBuilder, Scope};
1115    /// # use url::Url;
1116    /// # use std::borrow::Cow;
1117    /// # let client = twitch_oauth2::client::DummyClient; stringify!(
1118    /// let client = reqwest::Client::builder()
1119    ///     .redirect(reqwest::redirect::Policy::none())
1120    ///     .build()?;
1121    /// # );
1122    /// # let mut builder = DeviceUserTokenBuilder::new("myclientid", vec![Scope::ChatRead, Scope::ChatEdit]);
1123    /// let code = builder.start(&client).await?;
1124    /// println!("Please go to {}", code.verification_uri);
1125    /// let mut interval = tokio::time::interval(std::time::Duration::from_secs(code.interval));
1126    /// let mut finish = builder.try_finish(&client).await;
1127    /// while finish.as_ref().is_err_and(|e| e.is_pending()) {
1128    ///     // wait a bit
1129    ///     interval.tick().await;
1130    ///     finish = builder.try_finish(&client).await;
1131    /// }
1132    /// let token: UserToken = finish?;
1133    /// # Ok::<(), Box<dyn std::error::Error>>(())
1134    /// # };
1135    /// ```
1136    #[cfg(feature = "client")]
1137    pub async fn try_finish<C>(
1138        &self,
1139        http_client: &C,
1140    ) -> Result<UserToken, DeviceUserTokenExchangeError<C::Error>>
1141    where
1142        C: Client,
1143    {
1144        let req = self
1145            .get_user_token_request()
1146            .ok_or(DeviceUserTokenExchangeError::NoDeviceCode)?;
1147        let resp = http_client
1148            .req(req)
1149            .await
1150            .map_err(DeviceUserTokenExchangeError::TokenRequestError)?;
1151        let response = crate::id::TwitchTokenResponse::from_response(&resp)
1152            .map_err(DeviceUserTokenExchangeError::TokenParseError)?;
1153        let validated = response.access_token.validate_token(http_client).await?;
1154        // FIXME: get rid of the clone
1155        UserToken::from_response(response, validated, self.client_secret.clone())
1156            .map_err(|v| v.into_other().into())
1157    }
1158}
1159
1160#[cfg(test)]
1161mod tests {
1162    use crate::id::TwitchTokenResponse;
1163
1164    pub use super::*;
1165
1166    #[test]
1167    fn from_validated_and_token() {
1168        let body = br#"
1169        {
1170            "client_id": "wbmytr93xzw8zbg0p1izqyzzc5mbiz",
1171            "login": "twitchdev",
1172            "scopes": [
1173              "channel:read:subscriptions"
1174            ],
1175            "user_id": "141981764",
1176            "expires_in": 5520838
1177        }
1178        "#;
1179        let response = http::Response::builder().status(200).body(body).unwrap();
1180        let validated = ValidatedToken::from_response(&response).unwrap();
1181        let body = br#"
1182        {
1183            "access_token": "rfx2uswqe8l4g1mkagrvg5tv0ks3",
1184            "expires_in": 14124,
1185            "refresh_token": "5b93chm6hdve3mycz05zfzatkfdenfspp1h1ar2xxdalen01",
1186            "scope": [
1187                "channel:read:subscriptions"
1188            ],
1189            "token_type": "bearer"
1190          }
1191        "#;
1192        let response = http::Response::builder().status(200).body(body).unwrap();
1193        let response = TwitchTokenResponse::from_response(&response).unwrap();
1194
1195        UserToken::from_response(response, validated, None).unwrap();
1196    }
1197
1198    #[test]
1199    fn generate_url() {
1200        UserTokenBuilder::new(
1201            ClientId::from("random_client"),
1202            ClientSecret::from("random_secret"),
1203            url::Url::parse("https://localhost").unwrap(),
1204        )
1205        .force_verify(true)
1206        .generate_url()
1207        .0
1208        .to_string();
1209    }
1210
1211    #[tokio::test]
1212    #[ignore]
1213    #[cfg(feature = "reqwest")]
1214    async fn get_token() {
1215        let mut t = UserTokenBuilder::new(
1216            ClientId::new(
1217                std::env::var("TWITCH_CLIENT_ID").expect("no env:TWITCH_CLIENT_ID provided"),
1218            ),
1219            ClientSecret::new(
1220                std::env::var("TWITCH_CLIENT_SECRET")
1221                    .expect("no env:TWITCH_CLIENT_SECRET provided"),
1222            ),
1223            url::Url::parse(r#"https://localhost"#).unwrap(),
1224        )
1225        .force_verify(true);
1226        t.csrf = Some(crate::CsrfToken::from("random"));
1227        let token = t
1228            .get_user_token(&reqwest::Client::new(), "random", "authcode")
1229            .await
1230            .unwrap();
1231        println!("token: {:?} - {}", token, token.access_token.secret());
1232    }
1233
1234    #[tokio::test]
1235    #[ignore]
1236    #[cfg(feature = "reqwest")]
1237    async fn get_implicit_token() {
1238        let mut t = ImplicitUserTokenBuilder::new(
1239            ClientId::new(
1240                std::env::var("TWITCH_CLIENT_ID").expect("no env:TWITCH_CLIENT_ID provided"),
1241            ),
1242            url::Url::parse(r#"http://localhost/twitch/register"#).unwrap(),
1243        )
1244        .force_verify(true);
1245        println!("{}", t.generate_url().0);
1246        t.csrf = Some(crate::CsrfToken::from("random"));
1247        let token = t
1248            .get_user_token(
1249                &reqwest::Client::new(),
1250                Some("random"),
1251                Some("authcode"),
1252                None,
1253                None,
1254            )
1255            .await
1256            .unwrap();
1257        println!("token: {:?} - {}", token, token.access_token.secret());
1258    }
1259}