Skip to main content

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
382impl TwitchToken for UserToken {
383    fn token_type() -> super::BearerTokenType { super::BearerTokenType::UserToken }
384
385    fn client_id(&self) -> &ClientId { &self.client_id }
386
387    fn token(&self) -> &AccessToken { &self.access_token }
388
389    fn login(&self) -> Option<&UserNameRef> { Some(&self.login) }
390
391    fn user_id(&self) -> Option<&UserIdRef> { Some(&self.user_id) }
392
393    #[cfg(feature = "client")]
394    async fn refresh_token<'a, C>(
395        &mut self,
396        http_client: &'a C,
397    ) -> Result<(), RefreshTokenError<<C as Client>::Error>>
398    where
399        Self: Sized,
400        C: Client,
401    {
402        let (access_token, expires, refresh_token) = if let Some(token) = self.refresh_token.take()
403        {
404            token
405                .refresh_token(http_client, &self.client_id, self.client_secret.as_ref())
406                .await?
407        } else {
408            return Err(RefreshTokenError::NoRefreshToken);
409        };
410        self.access_token = access_token;
411        self.expires_in = expires;
412        self.refresh_token = refresh_token;
413        self.struct_created = Instant::now();
414        Ok(())
415    }
416
417    fn expires_in(&self) -> std::time::Duration {
418        if !self.never_expiring {
419            self.expires_in
420                .checked_sub(self.struct_created.elapsed())
421                .unwrap_or_default()
422        } else {
423            // We don't return an option here because it's not expected to use this if the token is known to be unexpiring.
424            std::time::Duration::MAX
425        }
426    }
427
428    fn scopes(&self) -> &[Scope] { self.scopes.as_slice() }
429}
430
431/// Builder for [OAuth authorization code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#authorization-code-grant-flow)
432///
433/// 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)
434///
435/// # Examples
436///
437/// See also [the auth flow example](https://github.com/twitch-rs/twitch_oauth2/blob/main/examples/auth_flow.rs)
438///
439/// To generate a user token with this auth flow, you need to:
440///
441/// 1. Initialize the [`UserTokenBuilder`] with [`UserTokenBuilder::new`](UserTokenBuilder::new), providing your client id, client secret, and a redirect URL.
442///    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
443///    re-authorize your app’s access to their resources.
444///
445///     Make sure you've added the redirect URL to the app settings on [the Twitch Developer Console](https://dev.twitch.tv/console).
446///
447///     ```rust
448///     use twitch_oauth2::{id::TwitchTokenResponse, tokens::UserTokenBuilder, Scope};
449///     use url::Url;
450///
451///     // This is the URL the user will be redirected to after authorizing your application
452///     let redirect_url = Url::parse("http://localhost/twitch/register")?;
453///     let mut builder = UserTokenBuilder::new("myclientid", "myclientsecret", redirect_url);
454///     builder = builder.set_scopes(vec![Scope::ChatRead, Scope::ChatEdit]);
455///     builder = builder.force_verify(true); // Defaults to false
456///     # Ok::<(), Box<dyn std::error::Error>>(())
457///     ```
458///
459/// 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.
460///
461///     ```rust
462///     # use twitch_oauth2::{id::TwitchTokenResponse, tokens::UserTokenBuilder, Scope};
463///     # use url::Url;
464///     # let redirect_url = Url::parse("http://localhost/twitch/register")?;
465///     # let mut builder = UserTokenBuilder::new("myclientid", "myclientsecret", redirect_url);
466///     let (url, csrf_token) = builder.generate_url();
467///     // Make your user navigate to this URL, for example
468///     println!("Visit this URL to authorize Twitch access: {}", url);
469///     # Ok::<(), Box<dyn std::error::Error>>(())
470///     ```
471///
472/// 3. Have the user visit the generated URL. They will be asked to authorize your application if they haven't previously done so
473///    or if you've set [`force_verify`](UserTokenBuilder::force_verify) to `true`.
474///
475///    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),
476///    the console, or by [opening it](https://docs.rs/webbrowser/0.8.10/webbrowser/) in a browser.
477///
478///    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)
479///    or a [`HashMap`](std::collections::HashMap) with the CSRF token as the key.
480///
481/// 4. When the user has been redirected to the redirect URL by twitch, extract the `state` and `code` query parameters from the URL.
482///
483///     ```rust
484///     use std::borrow::Cow;
485///     use std::collections::BTreeMap;
486///
487///     fn extract_pair<'a>(
488///         query: &BTreeMap<Cow<'a, str>, Cow<'a, str>>,
489///         key1: &str,
490///         key2: &str,
491///     ) -> Option<(Cow<'a, str>, Cow<'a, str>)> {
492///         Some((query.get(key1)?.clone(), query.get(key2)?.clone()))
493///     }
494///
495///     /// Extract the state and code from the URL a user was redirected to after authorizing the application.
496///     fn extract_url<'a>(
497///         url: &'a url::Url,
498///     ) -> Result<(Cow<'a, str>, Cow<'a, str>), Option<(Cow<'a, str>, Cow<'a, str>)>> {
499///         let query: BTreeMap<_, _> = url.query_pairs().collect();
500///         if let Some((error, error_description)) = extract_pair(&query, "error", "error_description") {
501///             Err(Some((error, error_description)))
502///         } else if let Some((state, code)) = extract_pair(&query, "state", "code") {
503///             Ok((state, code))
504///         } else {
505///             Err(None)
506///         }
507///     }
508///     ```
509/// 5. Finally, call [`get_user_token`](UserTokenBuilder::get_user_token) with the `state` and `code` query parameters to get the user's access token.
510///
511///     ```rust
512///     # async move {
513///     # use twitch_oauth2::{id::TwitchTokenResponse, tokens::UserTokenBuilder, Scope};
514///     # use url::Url;
515///     # use std::borrow::Cow;
516///     # let redirect_url = Url::parse("http://localhost/twitch/register")?;
517///     # let mut builder = UserTokenBuilder::new("myclientid", "myclientsecret", redirect_url);
518///     # let (url, csrf_token) = builder.generate_url();
519///     # fn extract_url<'a>(_: &'a url::Url) -> Result<(Cow<'a, str>, Cow<'a, str>), std::io::Error> { Ok((Cow::default(), Cow::default())) }
520///     # let url = url::Url::parse("http://localhost/twitch/register?code=code&state=state")?;
521///     # let client = twitch_oauth2::client::DummyClient; stringify!(
522///     let client = reqwest::Client::builder()
523///         .redirect(reqwest::redirect::Policy::none())
524///         .build()?;
525///     # );
526///     let (state, code) = extract_url(&url)?;
527///     let token = builder.get_user_token(&client, state.as_ref(), code.as_ref()).await?;
528///     println!("User token: {:?}", token);
529///     # Ok::<(), Box<dyn std::error::Error>>(())
530///     # };
531///     ```
532pub struct UserTokenBuilder {
533    pub(crate) scopes: Vec<Scope>,
534    pub(crate) csrf: Option<crate::types::CsrfToken>,
535    pub(crate) force_verify: bool,
536    pub(crate) redirect_url: url::Url,
537    client_id: ClientId,
538    client_secret: ClientSecret,
539}
540
541impl UserTokenBuilder {
542    /// Create a [`UserTokenBuilder`]
543    ///
544    /// # Notes
545    ///
546    /// The `redirect_url` must be present, verbatim, on [the Twitch Developer Console](https://dev.twitch.tv/console).
547    ///
548    /// The `url` crate converts empty paths into "/" (such as `https://example.com` into `https://example.com/`),
549    /// 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.
550    ///
551    /// 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 `/`.
552    pub fn new(
553        client_id: impl Into<ClientId>,
554        client_secret: impl Into<ClientSecret>,
555        redirect_url: url::Url,
556    ) -> UserTokenBuilder {
557        UserTokenBuilder {
558            scopes: vec![],
559            csrf: None,
560            force_verify: false,
561            redirect_url,
562            client_id: client_id.into(),
563            client_secret: client_secret.into(),
564        }
565    }
566
567    /// Add scopes to the request
568    pub fn set_scopes(mut self, scopes: Vec<Scope>) -> Self {
569        self.scopes = scopes;
570        self
571    }
572
573    /// Add a single scope to request
574    pub fn add_scope(&mut self, scope: Scope) { self.scopes.push(scope); }
575
576    /// Enable or disable function to make the user able to switch accounts if needed.
577    pub fn force_verify(mut self, b: bool) -> Self {
578        self.force_verify = b;
579        self
580    }
581
582    /// Generate the URL to request a code.
583    ///
584    /// First step in the [guide](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#get-the-user-to-authorize-your-app)
585    pub fn generate_url(&mut self) -> (url::Url, crate::types::CsrfToken) {
586        let csrf = crate::types::CsrfToken::new_random();
587        self.csrf = Some(csrf.clone());
588        let mut url = crate::AUTH_URL.clone();
589
590        let auth = vec![
591            ("response_type", "code"),
592            ("client_id", self.client_id.as_str()),
593            ("redirect_uri", self.redirect_url.as_str()),
594            ("state", csrf.as_str()),
595        ];
596
597        url.query_pairs_mut().extend_pairs(auth);
598
599        if !self.scopes.is_empty() {
600            url.query_pairs_mut()
601                .append_pair("scope", &self.scopes.as_slice().join(" "));
602        }
603
604        if self.force_verify {
605            url.query_pairs_mut().append_pair("force_verify", "true");
606        };
607
608        (url, csrf)
609    }
610
611    /// Set the CSRF token.
612    ///
613    /// Hidden because you should preferably not use this.
614    #[doc(hidden)]
615    pub fn set_csrf(&mut self, csrf: crate::types::CsrfToken) { self.csrf = Some(csrf); }
616
617    /// Check if the CSRF is valid
618    pub fn csrf_is_valid(&self, csrf: &str) -> bool {
619        if let Some(csrf2) = &self.csrf {
620            csrf2.secret() == csrf
621        } else {
622            false
623        }
624    }
625
626    /// Get the request for getting a [TwitchTokenResponse](crate::id::TwitchTokenResponse), to be used in [UserToken::from_response].
627    ///
628    /// # Examples
629    ///
630    /// ```rust
631    /// use twitch_oauth2::{tokens::UserTokenBuilder, id::TwitchTokenResponse};
632    /// use url::Url;
633    /// let callback_url = Url::parse("http://localhost/twitch/register")?;
634    /// let mut builder = UserTokenBuilder::new("myclientid", "myclientsecret", callback_url);
635    /// let (url, _csrf_code) = builder.generate_url();
636    ///
637    /// // Direct the user to this url.
638    /// // Later when your server gets a response on `callback_url` with `?code=xxxxxxx&state=xxxxxxx&scope=aa%3Aaa+bb%3Abb`
639    ///
640    /// // validate the state
641    /// # let state_in_query = _csrf_code.secret();
642    /// if !builder.csrf_is_valid(state_in_query) {
643    ///     panic!("state mismatched")
644    /// }
645    /// // and then get your token
646    /// # let code_in_query = _csrf_code.secret();
647    /// let request = builder.get_user_token_request(code_in_query);
648    ///
649    /// // use your favorite http client
650    ///
651    /// let response: http::Response<Vec<u8>> = client_req(request);
652    /// let twitch_response = TwitchTokenResponse::from_response(&response)?;
653    ///
654    /// // you now have a access token, do what you want with it.
655    /// // You're recommended to convert it into a `UserToken` via `UserToken::from_response`
656    ///
657    /// // You can validate the access_token like this
658    /// let validated_req = twitch_response.access_token.validate_token_request();
659    /// # fn client_req(_: http::Request<Vec<u8>>) -> http::Response<Vec<u8>> { http::Response::new(
660    /// # r#"{"access_token":"rfx2uswqe8l4g1mkagrvg5tv0ks3","expires_in":14124,"refresh_token":"5b93chm6hdve3mycz05zfzatkfdenfspp1h1ar2xxdalen01","scope":["channel:moderate","chat:edit","chat:read"],"token_type":"bearer"}"#.bytes().collect()
661    /// # ) }
662    /// # Ok::<(), Box<dyn std::error::Error>>(())
663    /// ```
664    pub fn get_user_token_request(&self, code: &str) -> http::Request<Vec<u8>> {
665        use http::{HeaderMap, Method};
666        use std::collections::HashMap;
667        let mut params = HashMap::new();
668        params.insert("client_id", self.client_id.as_str());
669        params.insert("client_secret", self.client_secret.secret());
670        params.insert("code", code);
671        params.insert("grant_type", "authorization_code");
672        params.insert("redirect_uri", self.redirect_url.as_str());
673
674        crate::construct_request(
675            &crate::TOKEN_URL,
676            &params,
677            HeaderMap::new(),
678            Method::POST,
679            vec![],
680        )
681    }
682
683    /// Generate the code with the help of the authorization code
684    ///
685    /// Last step in the [guide](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#use-the-authorization-code-to-get-a-token)
686    ///
687    /// 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>`
688    #[cfg(feature = "client")]
689    pub async fn get_user_token<C>(
690        self,
691        http_client: &C,
692        state: &str,
693        // TODO: Should be either str or AuthorizationCode
694        code: &str,
695    ) -> Result<UserToken, UserTokenExchangeError<<C as Client>::Error>>
696    where
697        C: Client,
698    {
699        if !self.csrf_is_valid(state) {
700            return Err(UserTokenExchangeError::StateMismatch);
701        }
702
703        let req = self.get_user_token_request(code);
704
705        let resp = http_client
706            .req(req)
707            .await
708            .map_err(UserTokenExchangeError::RequestError)?;
709
710        let response = crate::id::TwitchTokenResponse::from_response(&resp)?;
711        let validated = response.access_token.validate_token(http_client).await?;
712
713        UserToken::from_response(response, validated, self.client_secret)
714            .map_err(|e| e.into_other().into())
715    }
716}
717
718/// Builder for [OAuth implicit code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#implicit-grant-flow)
719///
720/// 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)
721pub struct ImplicitUserTokenBuilder {
722    pub(crate) scopes: Vec<Scope>,
723    pub(crate) csrf: Option<crate::types::CsrfToken>,
724    pub(crate) redirect_url: url::Url,
725    pub(crate) force_verify: bool,
726    client_id: ClientId,
727}
728
729impl ImplicitUserTokenBuilder {
730    /// Create a [`ImplicitUserTokenBuilder`]
731    ///
732    /// # Notes
733    ///
734    /// The `redirect_url` must be present, verbatim, on [the Twitch Developer Console](https://dev.twitch.tv/console).
735    ///
736    /// The `url` crate converts empty paths into "/" (such as `https://example.com` into `https://example.com/`),
737    /// 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.
738    ///
739    /// 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 `/`.
740    pub fn new(client_id: ClientId, redirect_url: url::Url) -> ImplicitUserTokenBuilder {
741        ImplicitUserTokenBuilder {
742            scopes: vec![],
743            redirect_url,
744            csrf: None,
745            force_verify: false,
746            client_id,
747        }
748    }
749
750    /// Add scopes to the request
751    pub fn set_scopes(mut self, scopes: Vec<Scope>) -> Self {
752        self.scopes = scopes;
753        self
754    }
755
756    /// Add a single scope to request
757    pub fn add_scope(&mut self, scope: Scope) { self.scopes.push(scope); }
758
759    /// Enable or disable function to make the user able to switch accounts if needed.
760    pub fn force_verify(mut self, b: bool) -> Self {
761        self.force_verify = b;
762        self
763    }
764
765    /// Generate the URL to request a token.
766    ///
767    /// First step in the [guide](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#implicit-grant-flow)
768    pub fn generate_url(&mut self) -> (url::Url, crate::types::CsrfToken) {
769        let csrf = crate::types::CsrfToken::new_random();
770        self.csrf = Some(csrf.clone());
771        let mut url = crate::AUTH_URL.clone();
772
773        let auth = vec![
774            ("response_type", "token"),
775            ("client_id", self.client_id.as_str()),
776            ("redirect_uri", self.redirect_url.as_str()),
777            ("state", csrf.as_str()),
778        ];
779
780        url.query_pairs_mut().extend_pairs(auth);
781
782        if !self.scopes.is_empty() {
783            url.query_pairs_mut()
784                .append_pair("scope", &self.scopes.as_slice().join(" "));
785        }
786
787        if self.force_verify {
788            url.query_pairs_mut().append_pair("force_verify", "true");
789        };
790
791        (url, csrf)
792    }
793
794    /// Check if the CSRF is valid
795    pub fn csrf_is_valid(&self, csrf: &str) -> bool {
796        if let Some(csrf2) = &self.csrf {
797            csrf2.secret() == csrf
798        } else {
799            false
800        }
801    }
802
803    /// Generate the code with the help of the hash.
804    ///
805    /// 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.
806    ///
807    /// Last step in the [guide](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#implicit-grant-flow)
808    ///
809    /// # Example
810    ///
811    /// When the user authenticates, they are sent to `<redirecturl>#access_token=<access_token>&scope=<scopes, space (%20) separated>&state=<csrf state>&token_type=bearer`
812    ///
813    /// On failure, they are sent to
814    ///
815    /// `<redirect_url or first defined url in dev console>?error=<error type>&error_description=<error description>&state=<csrf state>`
816    /// Get the hash of the url with javascript.
817    ///
818    /// ```js
819    /// document.location.hash.substr(1);
820    /// ```
821    ///
822    /// and send it to your client in what ever way convenient.
823    ///
824    /// Provided below is an example of how to do it, no guarantees on the safety of this method.
825    ///
826    /// ```html
827    /// <!DOCTYPE html>
828    /// <html>
829    /// <head>
830    /// <title>Authorization</title>
831    /// <meta name="ROBOTS" content="NOFOLLOW">
832    /// <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
833    /// <script type="text/javascript">
834    /// <!--
835    /// function initiate() {
836    ///     var hash = document.location.hash.substr(1);
837    ///     document.getElementById("javascript").className = "";
838    ///     if (hash != null) {
839    ///             document.location.replace("/token?"+hash);
840    ///     }
841    ///     else {
842    ///         document.getElementById("javascript").innerHTML = "Error: Access Token not found";
843    ///     }
844    /// }
845    /// -->
846    /// </script>
847    /// <style type="text/css">
848    ///     body { text-align: center; background-color: #FFF; max-width: 500px; margin: auto; }
849    ///     noscript { color: red;  }
850    ///     .hide { display: none; }
851    /// </style>
852    /// </head>
853    /// <body onload="initiate()">
854    /// <h1>Authorization</h1>
855    /// <noscript>
856    ///     <p>This page requires <strong>JavaScript</strong> to get your token.
857    /// </noscript>
858    /// <p id="javascript" class="hide">
859    /// You should be redirected..
860    /// </p>
861    /// </body>
862    /// </html>
863    /// ```
864    ///
865    /// where `/token?` gives this function it's corresponding arguments in query params
866    ///
867    /// Make sure that `/token` removes the query from the history.
868    ///
869    /// ```html
870    /// <!DOCTYPE html>
871    /// <html>
872    /// <head>
873    /// <title>Authorization Successful</title>
874    /// <meta name="ROBOTS" content="NOFOLLOW">
875    /// <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
876    /// <script type="text/javascript">
877    /// <!--
878    /// function initiate() {
879    ///     //
880    ///     document.location.replace("/token_retrieved);
881    /// }
882    /// -->
883    /// </script>
884    /// <style type="text/css">
885    ///     body { text-align: center; background-color: #FFF; max-width: 500px; margin: auto; }
886    /// </style>
887    /// </head>
888    /// <body onload="initiate()">
889    /// <h1>Authorization Successful</h1>
890    /// </body>
891    /// </html>
892    /// ```
893    #[cfg(feature = "client")]
894    pub async fn get_user_token<C>(
895        self,
896        http_client: &C,
897        state: Option<&str>,
898        access_token: Option<&str>,
899        error: Option<&str>,
900        error_description: Option<&str>,
901    ) -> Result<UserToken, ImplicitUserTokenExchangeError<<C as Client>::Error>>
902    where
903        C: Client,
904    {
905        if !state.map(|s| self.csrf_is_valid(s)).unwrap_or_default() {
906            return Err(ImplicitUserTokenExchangeError::StateMismatch);
907        }
908
909        match (access_token, error, error_description) {
910            (Some(access_token), None, None) => UserToken::from_existing(
911                http_client,
912                crate::types::AccessToken::from(access_token),
913                None,
914                None,
915            )
916            .await
917            .map_err(From::from),
918            (_, error, description) => {
919                let (error, description) = (
920                    error.map(|s| s.to_string()),
921                    description.map(|s| s.to_string()),
922                );
923                Err(ImplicitUserTokenExchangeError::TwitchError { error, description })
924            }
925        }
926    }
927}
928
929/// Builder for [OAuth device code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#device-flow)
930///
931/// # Examples
932///
933/// ```rust
934/// # async move {
935/// # use twitch_oauth2::{id::TwitchTokenResponse, UserToken, tokens::DeviceUserTokenBuilder, Scope};
936/// # use url::Url;
937/// # use std::borrow::Cow;
938/// # let client = twitch_oauth2::client::DummyClient; stringify!(
939/// let client = reqwest::Client::builder()
940///     .redirect(reqwest::redirect::Policy::none())
941///     .build()?;
942/// # );
943/// let mut builder = DeviceUserTokenBuilder::new("myclientid", vec![Scope::ChatRead, Scope::ChatEdit]);
944/// let code = builder.start(&client).await?;
945/// println!("Please go to {}", code.verification_uri);
946/// let token = builder.wait_for_code(&client, tokio::time::sleep).await?;
947/// println!("Token: {:?}", token);
948/// # Ok::<(), Box<dyn std::error::Error>>(())
949/// # };
950/// ```
951pub struct DeviceUserTokenBuilder {
952    client_id: ClientId,
953    client_secret: Option<ClientSecret>,
954    scopes: Vec<Scope>,
955    response: Option<(Instant, crate::id::DeviceCodeResponse)>,
956}
957
958impl DeviceUserTokenBuilder {
959    /// Create a [`DeviceUserTokenBuilder`]
960    pub fn new(client_id: impl Into<ClientId>, scopes: Vec<Scope>) -> Self {
961        Self {
962            client_id: client_id.into(),
963            client_secret: None,
964            scopes,
965            response: None,
966        }
967    }
968
969    /// Set the client secret, only necessary if you have one
970    pub fn set_secret(&mut self, secret: Option<ClientSecret>) { self.client_secret = secret; }
971
972    /// Get the request for getting a [`DeviceCodeResponse`](crate::id::DeviceCodeResponse)
973    pub fn get_exchange_device_code_request(&self) -> http::Request<Vec<u8>> {
974        // the equivalent of curl --location 'https://id.twitch.tv/oauth2/device' \
975        // --form 'client_id="<clientID>"' \
976        // --form 'scopes="<scopes>"'
977        use http::{HeaderMap, Method};
978        use std::collections::HashMap;
979        let mut params = HashMap::new();
980        params.insert("client_id", self.client_id.as_str());
981        let scopes = self.scopes.as_slice().join(" ");
982        if !scopes.is_empty() {
983            params.insert("scopes", &scopes);
984        }
985        crate::construct_request(
986            &crate::DEVICE_URL,
987            params,
988            HeaderMap::new(),
989            Method::POST,
990            vec![],
991        )
992    }
993
994    /// Parse the response from the device code request
995    pub fn parse_exchange_device_code_response(
996        &mut self,
997        response: http::Response<Vec<u8>>,
998    ) -> Result<&crate::id::DeviceCodeResponse, crate::RequestParseError> {
999        let response = crate::parse_response(&response)?;
1000        self.response = Some((Instant::now(), response));
1001        Ok(&self.response.as_ref().unwrap().1)
1002    }
1003
1004    /// Start the device code flow
1005    ///
1006    /// # Notes
1007    ///
1008    /// Use [`DeviceCodeResponse::verification_uri`](crate::id::DeviceCodeResponse::verification_uri) to get the URL the user needs to visit.
1009    #[cfg(feature = "client")]
1010    pub async fn start<'s, C>(
1011        &'s mut self,
1012        http_client: &C,
1013    ) -> Result<&'s crate::id::DeviceCodeResponse, DeviceUserTokenExchangeError<C::Error>>
1014    where
1015        C: Client,
1016    {
1017        let req = self.get_exchange_device_code_request();
1018        let resp = http_client
1019            .req(req)
1020            .await
1021            .map_err(DeviceUserTokenExchangeError::DeviceExchangeRequestError)?;
1022        self.parse_exchange_device_code_response(resp)
1023            .map_err(DeviceUserTokenExchangeError::DeviceExchangeParseError)
1024    }
1025
1026    /// Get the request for getting a [`TwitchTokenResponse`](crate::id::TwitchTokenResponse), to be used in [UserToken::from_response].
1027    ///
1028    /// Returns None if there is no `device_code`
1029    pub fn get_user_token_request(&self) -> Option<http::Request<Vec<u8>>> {
1030        use http::{HeaderMap, Method};
1031        use std::collections::HashMap;
1032        let Some((_, response)) = &self.response else {
1033            return None;
1034        };
1035        let mut params = HashMap::new();
1036        params.insert("client_id", self.client_id.as_str());
1037        params.insert("device_code", &response.device_code);
1038        params.insert("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
1039
1040        Some(crate::construct_request(
1041            &crate::TOKEN_URL,
1042            &params,
1043            HeaderMap::new(),
1044            Method::POST,
1045            vec![],
1046        ))
1047    }
1048
1049    /// Finish the device code flow by waiting for the user to authorize, granting you a token if the user has authorized the app.
1050    ///
1051    /// Will return [`DeviceUserTokenExchangeError::Expired`] if the user has not authorized the app within the [`expires_in`](crate::id::DeviceCodeResponse::expires_in) time.
1052    ///
1053    /// # Examples
1054    ///
1055    /// ```rust
1056    /// # async move {
1057    /// # use twitch_oauth2::{id::TwitchTokenResponse, UserToken, tokens::DeviceUserTokenBuilder, Scope};
1058    /// # use url::Url;
1059    /// # use std::borrow::Cow;
1060    /// # let client = twitch_oauth2::client::DummyClient; stringify!(
1061    /// let client = reqwest::Client::builder()
1062    ///     .redirect(reqwest::redirect::Policy::none())
1063    ///     .build()?;
1064    /// # );
1065    /// let mut builder = DeviceUserTokenBuilder::new("myclientid", vec![Scope::ChatRead, Scope::ChatEdit]);
1066    /// let code = builder.start(&client).await?;
1067    /// println!("Please go to {}", code.verification_uri);
1068    /// let token = builder.wait_for_code(&client, tokio::time::sleep).await?;
1069    /// println!("Token: {:?}", token);
1070    /// # Ok::<(), Box<dyn std::error::Error>>(())
1071    /// # };
1072    /// ```
1073    #[cfg(feature = "client")]
1074    pub async fn wait_for_code<C, Fut>(
1075        &mut self,
1076        client: &C,
1077        wait_fn: impl Fn(std::time::Duration) -> Fut,
1078    ) -> Result<UserToken, DeviceUserTokenExchangeError<C::Error>>
1079    where
1080        C: Client,
1081        Fut: std::future::Future<Output = ()>,
1082    {
1083        let (created, response) = self
1084            .response
1085            .as_ref()
1086            .ok_or(DeviceUserTokenExchangeError::NoDeviceCode)?;
1087        let mut finish = self.try_finish(client).await;
1088        while finish.as_ref().is_err_and(|e| e.is_pending()) {
1089            wait_fn(std::time::Duration::from_secs(response.interval)).await;
1090            finish = self.try_finish(client).await;
1091            if created.elapsed() > std::time::Duration::from_secs(response.expires_in) {
1092                return Err(DeviceUserTokenExchangeError::Expired);
1093            }
1094        }
1095
1096        let token = finish?;
1097        Ok(token)
1098    }
1099
1100    /// Finish the device code flow, granting you a token if the user has authorized the app.
1101    /// Consider using the [`wait_for_code`](Self::wait_for_code) method instead.
1102    ///
1103    /// # Notes
1104    ///
1105    /// Must be called after [`start`](Self::start) and will return an error if not.
1106    /// 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.
1107    /// To check for this condition, use the [`is_pending`](DeviceUserTokenExchangeError::is_pending) method.
1108    ///
1109    /// # Examples
1110    ///
1111    /// ```rust
1112    /// # async move {
1113    /// # use twitch_oauth2::{id::TwitchTokenResponse, UserToken, tokens::DeviceUserTokenBuilder, Scope};
1114    /// # use url::Url;
1115    /// # use std::borrow::Cow;
1116    /// # let client = twitch_oauth2::client::DummyClient; stringify!(
1117    /// let client = reqwest::Client::builder()
1118    ///     .redirect(reqwest::redirect::Policy::none())
1119    ///     .build()?;
1120    /// # );
1121    /// # let mut builder = DeviceUserTokenBuilder::new("myclientid", vec![Scope::ChatRead, Scope::ChatEdit]);
1122    /// let code = builder.start(&client).await?;
1123    /// println!("Please go to {}", code.verification_uri);
1124    /// let mut interval = tokio::time::interval(std::time::Duration::from_secs(code.interval));
1125    /// let mut finish = builder.try_finish(&client).await;
1126    /// while finish.as_ref().is_err_and(|e| e.is_pending()) {
1127    ///     // wait a bit
1128    ///     interval.tick().await;
1129    ///     finish = builder.try_finish(&client).await;
1130    /// }
1131    /// let token: UserToken = finish?;
1132    /// # Ok::<(), Box<dyn std::error::Error>>(())
1133    /// # };
1134    /// ```
1135    #[cfg(feature = "client")]
1136    pub async fn try_finish<C>(
1137        &self,
1138        http_client: &C,
1139    ) -> Result<UserToken, DeviceUserTokenExchangeError<C::Error>>
1140    where
1141        C: Client,
1142    {
1143        let req = self
1144            .get_user_token_request()
1145            .ok_or(DeviceUserTokenExchangeError::NoDeviceCode)?;
1146        let resp = http_client
1147            .req(req)
1148            .await
1149            .map_err(DeviceUserTokenExchangeError::TokenRequestError)?;
1150        let response = crate::id::TwitchTokenResponse::from_response(&resp)
1151            .map_err(DeviceUserTokenExchangeError::TokenParseError)?;
1152        let validated = response.access_token.validate_token(http_client).await?;
1153        // FIXME: get rid of the clone
1154        UserToken::from_response(response, validated, self.client_secret.clone())
1155            .map_err(|v| v.into_other().into())
1156    }
1157}
1158
1159#[cfg(test)]
1160mod tests {
1161    use crate::id::TwitchTokenResponse;
1162
1163    pub use super::*;
1164
1165    #[test]
1166    fn from_validated_and_token() {
1167        let body = br#"
1168        {
1169            "client_id": "wbmytr93xzw8zbg0p1izqyzzc5mbiz",
1170            "login": "twitchdev",
1171            "scopes": [
1172              "channel:read:subscriptions"
1173            ],
1174            "user_id": "141981764",
1175            "expires_in": 5520838
1176        }
1177        "#;
1178        let response = http::Response::builder().status(200).body(body).unwrap();
1179        let validated = ValidatedToken::from_response(&response).unwrap();
1180        let body = br#"
1181        {
1182            "access_token": "rfx2uswqe8l4g1mkagrvg5tv0ks3",
1183            "expires_in": 14124,
1184            "refresh_token": "5b93chm6hdve3mycz05zfzatkfdenfspp1h1ar2xxdalen01",
1185            "scope": [
1186                "channel:read:subscriptions"
1187            ],
1188            "token_type": "bearer"
1189          }
1190        "#;
1191        let response = http::Response::builder().status(200).body(body).unwrap();
1192        let response = TwitchTokenResponse::from_response(&response).unwrap();
1193
1194        UserToken::from_response(response, validated, None).unwrap();
1195    }
1196
1197    #[test]
1198    fn generate_url() {
1199        UserTokenBuilder::new(
1200            ClientId::from("random_client"),
1201            ClientSecret::from("random_secret"),
1202            url::Url::parse("https://localhost").unwrap(),
1203        )
1204        .force_verify(true)
1205        .generate_url()
1206        .0
1207        .to_string();
1208    }
1209
1210    #[tokio::test]
1211    #[ignore]
1212    #[cfg(feature = "reqwest")]
1213    async fn get_token() {
1214        let mut t = UserTokenBuilder::new(
1215            ClientId::new(
1216                std::env::var("TWITCH_CLIENT_ID").expect("no env:TWITCH_CLIENT_ID provided"),
1217            ),
1218            ClientSecret::new(
1219                std::env::var("TWITCH_CLIENT_SECRET")
1220                    .expect("no env:TWITCH_CLIENT_SECRET provided"),
1221            ),
1222            url::Url::parse(r#"https://localhost"#).unwrap(),
1223        )
1224        .force_verify(true);
1225        t.csrf = Some(crate::CsrfToken::from("random"));
1226        let token = t
1227            .get_user_token(&reqwest::Client::new(), "random", "authcode")
1228            .await
1229            .unwrap();
1230        println!("token: {:?} - {}", token, token.access_token.secret());
1231    }
1232
1233    #[tokio::test]
1234    #[ignore]
1235    #[cfg(feature = "reqwest")]
1236    async fn get_implicit_token() {
1237        let mut t = ImplicitUserTokenBuilder::new(
1238            ClientId::new(
1239                std::env::var("TWITCH_CLIENT_ID").expect("no env:TWITCH_CLIENT_ID provided"),
1240            ),
1241            url::Url::parse(r#"http://localhost/twitch/register"#).unwrap(),
1242        )
1243        .force_verify(true);
1244        println!("{}", t.generate_url().0);
1245        t.csrf = Some(crate::CsrfToken::from("random"));
1246        let token = t
1247            .get_user_token(
1248                &reqwest::Client::new(),
1249                Some("random"),
1250                Some("authcode"),
1251                None,
1252                None,
1253            )
1254            .await
1255            .unwrap();
1256        println!("token: {:?} - {}", token, token.access_token.secret());
1257    }
1258}