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