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