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