twitch_oauth2/tokens/
user_token.rs

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