twitch_oauth2/lib.rs
1#![allow(unknown_lints, renamed_and_removed_lints)]
2#![deny(missing_docs, broken_intra_doc_links)] // This will be weird until 1.52, see https://github.com/rust-lang/rust/pull/80527
3#![cfg_attr(nightly, deny(rustdoc::broken_intra_doc_links))]
4#![cfg_attr(nightly, feature(doc_cfg))]
5#![cfg_attr(nightly, feature(doc_auto_cfg))]
6//! [![github]](https://github.com/twitch-rs/twitch_oauth2) [![crates-io]](https://crates.io/crates/twitch_oauth2) [![docs-rs]](https://docs.rs/twitch_oauth2/0.8.0/twitch_oauth2)
7//!
8//! [github]: https://img.shields.io/badge/github-twitch--rs/twitch__oauth2-8da0cb?style=for-the-badge&labelColor=555555&logo=github"
9//! [crates-io]: https://img.shields.io/crates/v/twitch_oauth2.svg?style=for-the-badge&color=fc8d62&logo=rust"
10//! [docs-rs]: https://img.shields.io/badge/docs.rs-twitch__oauth2-66c2a5?style=for-the-badge&labelColor=555555&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K"
11//!
12//! <br>
13//!
14//! <h5>OAuth2 for Twitch endpoints</h5>
15//!
16//! ```rust,no_run
17//! use twitch_oauth2::{tokens::errors::ValidationError, AccessToken, TwitchToken, UserToken};
18//! // Make sure you enable the feature "reqwest" for twitch_oauth2 if you want to use reqwest
19//! # async {let client = twitch_oauth2::client::DummyClient; stringify!(
20//! let client = reqwest::Client::builder()
21//! .redirect(reqwest::redirect::Policy::none())
22//! .build()?;
23//! # );
24//! let token = AccessToken::new("sometokenherewhichisvalidornot".to_string());
25//! let token = UserToken::from_token(&client, token).await?;
26//! println!("token: {:?}", token.token()); // prints `[redacted access token]`
27//! # Ok::<(), Box<dyn std::error::Error>>(())};
28//! ```
29//!
30//! # About
31//!
32//! ## Scopes
33//!
34//! The library contains all known twitch oauth2 scopes in [`Scope`].
35//!
36//! ## User Access Tokens
37//!
38//! For most basic use cases with user authorization, [`UserToken::from_token`] will be your main way
39//! to create user tokens in this library.
40//!
41//! Things like [`UserTokenBuilder`] can be used to create a token from scratch, via the [OAuth authorization code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#authorization-code-grant-flow)
42//! You can also use the newer [OAuth device code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#device-code-grant-flow) with [`DeviceUserTokenBuilder`].
43//!
44//! ## App access token
45//!
46//! Similar to [`UserToken`], a token with authorization as the twitch application can be created with
47//! [`AppAccessToken::get_app_access_token`].
48//!
49//! ## HTTP Requests
50//!
51//! To enable client features with a supported http library, enable the http library feature in `twitch_oauth2`, like `twitch_oauth2 = { features = ["reqwest"], version = "0.15.1" }`.
52//! If you're using [twitch_api](https://crates.io/crates/twitch_api), you can use its [`HelixClient`](https://docs.rs/twitch_api/latest/twitch_api/struct.HelixClient.html) instead of the underlying http client.
53//!
54//!
55//! This library can be used without any specific http client library (like if you don't want to use `await`),
56//! using methods like [`AppAccessToken::from_response`] and [`AppAccessToken::get_app_access_token_request`]
57//! or [`UserTokenBuilder::get_user_token_request`] and [`UserToken::from_response`]
58#[cfg(feature = "client")]
59pub mod client;
60pub mod id;
61pub mod scopes;
62pub mod tokens;
63pub mod types;
64
65use http::StatusCode;
66use id::TwitchTokenErrorResponse;
67#[cfg(feature = "client")]
68use tokens::errors::{RefreshTokenError, RevokeTokenError, ValidationError};
69
70#[doc(inline)]
71pub use scopes::{Scope, Validator};
72#[doc(inline)]
73pub use tokens::{
74 AppAccessToken, DeviceUserTokenBuilder, ImplicitUserTokenBuilder, TwitchToken, UserToken,
75 UserTokenBuilder, ValidatedToken,
76};
77
78pub use url;
79
80pub use types::{AccessToken, ClientId, ClientSecret, CsrfToken, RefreshToken};
81
82#[doc(hidden)]
83pub use types::{AccessTokenRef, ClientIdRef, ClientSecretRef, CsrfTokenRef, RefreshTokenRef};
84
85#[cfg(feature = "client")]
86use self::client::Client;
87
88/// Generate a url with a default if `mock_api` feature is disabled, or env var is not defined or is invalid utf8
89macro_rules! mock_env_url {
90 ($var:literal, $default:expr $(,)?) => {
91 once_cell::sync::Lazy::new(move || {
92 #[cfg(feature = "mock_api")]
93 if let Ok(url) = std::env::var($var) {
94 return url::Url::parse(&url).expect(concat!(
95 "URL could not be made from `env:",
96 $var,
97 "`."
98 ));
99 };
100 url::Url::parse(&$default).unwrap()
101 })
102 };
103}
104
105/// Defines the root path to twitch auth endpoints
106static TWITCH_OAUTH2_URL: once_cell::sync::Lazy<url::Url> =
107 mock_env_url!("TWITCH_OAUTH2_URL", "https://id.twitch.tv/oauth2/");
108
109/// Authorization URL (`https://id.twitch.tv/oauth2/authorize`) for `id.twitch.tv`
110///
111/// Can be overridden when feature `mock_api` is enabled with environment variable `TWITCH_OAUTH2_URL` to set the root path, or with `TWITCH_OAUTH2_AUTH_URL` to override the base (`https://id.twitch.tv/oauth2/`) url.
112///
113/// # Examples
114///
115/// Set the environment variable `TWITCH_OAUTH2_URL` to `http://localhost:8080/auth/` to use [`twitch-cli` mock](https://github.com/twitchdev/twitch-cli/blob/main/docs/mock-api.md) endpoints.
116pub static AUTH_URL: once_cell::sync::Lazy<url::Url> = mock_env_url!("TWITCH_OAUTH2_AUTH_URL", {
117 TWITCH_OAUTH2_URL.to_string() + "authorize"
118},);
119/// Token URL (`https://id.twitch.tv/oauth2/token`) for `id.twitch.tv`
120///
121/// Can be overridden when feature `mock_api` is enabled with environment variable `TWITCH_OAUTH2_URL` to set the root path, or with `TWITCH_OAUTH2_TOKEN_URL` to override the base (`https://id.twitch.tv/oauth2/`) url.
122///
123/// # Examples
124///
125/// Set the environment variable `TWITCH_OAUTH2_URL` to `http://localhost:8080/auth/` to use [`twitch-cli` mock](https://github.com/twitchdev/twitch-cli/blob/main/docs/mock-api.md) endpoints.
126pub static TOKEN_URL: once_cell::sync::Lazy<url::Url> = mock_env_url!("TWITCH_OAUTH2_TOKEN_URL", {
127 TWITCH_OAUTH2_URL.to_string() + "token"
128},);
129/// Device URL (`https://id.twitch.tv/oauth2/device`) for `id.twitch.tv`
130///
131/// Can be overridden when feature `mock_api` is enabled with environment variable `TWITCH_OAUTH2_URL` to set the root path, or with `TWITCH_OAUTH2_DEVICE_URL` to override the base (`https://id.twitch.tv/oauth2/`) url.
132///
133/// # Examples
134///
135/// Set the environment variable `TWITCH_OAUTH2_URL` to `http://localhost:8080/auth/` to use [`twitch-cli` mock](https://github.com/twitchdev/twitch-cli/blob/main/docs/mock-api.md) endpoints.
136pub static DEVICE_URL: once_cell::sync::Lazy<url::Url> =
137 mock_env_url!("TWITCH_OAUTH2_DEVICE_URL", {
138 TWITCH_OAUTH2_URL.to_string() + "device"
139 },);
140/// Validation URL (`https://id.twitch.tv/oauth2/validate`) for `id.twitch.tv`
141///
142/// Can be overridden when feature `mock_api` is enabled with environment variable `TWITCH_OAUTH2_URL` to set the root path, or with `TWITCH_OAUTH2_VALIDATE_URL` to override the base (`https://id.twitch.tv/oauth2/`) url.
143///
144/// # Examples
145///
146/// Set the environment variable `TWITCH_OAUTH2_URL` to `http://localhost:8080/auth/` to use [`twitch-cli` mock](https://github.com/twitchdev/twitch-cli/blob/main/docs/mock-api.md) endpoints.
147pub static VALIDATE_URL: once_cell::sync::Lazy<url::Url> =
148 mock_env_url!("TWITCH_OAUTH2_VALIDATE_URL", {
149 TWITCH_OAUTH2_URL.to_string() + "validate"
150 },);
151/// Revokation URL (`https://id.twitch.tv/oauth2/revoke`) for `id.twitch.tv`
152///
153/// Can be overridden when feature `mock_api` is enabled with environment variable `TWITCH_OAUTH2_URL` to set the root path, or with `TWITCH_OAUTH2_REVOKE_URL` to override the base (`https://id.twitch.tv/oauth2/`) url.
154///
155/// # Examples
156///
157/// Set the environment variable `TWITCH_OAUTH2_URL` to `http://localhost:8080/auth/` to use [`twitch-cli` mock](https://github.com/twitchdev/twitch-cli/blob/main/docs/mock-api.md) endpoints.
158pub static REVOKE_URL: once_cell::sync::Lazy<url::Url> =
159 mock_env_url!("TWITCH_OAUTH2_REVOKE_URL", {
160 TWITCH_OAUTH2_URL.to_string() + "revoke"
161 },);
162
163impl AccessTokenRef {
164 /// Get the request needed to validate this token.
165 ///
166 /// Parse the response from this endpoint with [ValidatedToken::from_response](crate::ValidatedToken::from_response)
167 pub fn validate_token_request(&self) -> http::Request<Vec<u8>> {
168 use http::{header::AUTHORIZATION, HeaderMap, Method};
169
170 let auth_header = format!("OAuth {}", self.secret());
171 let mut headers = HeaderMap::new();
172 headers.insert(
173 AUTHORIZATION,
174 auth_header
175 .parse()
176 .expect("Failed to parse header for validation"),
177 );
178
179 crate::construct_request::<&[(String, String)], _, _>(
180 &crate::VALIDATE_URL,
181 &[],
182 headers,
183 Method::GET,
184 vec![],
185 )
186 }
187
188 /// Validate this token.
189 ///
190 /// Should be checked on regularly, according to <https://dev.twitch.tv/docs/authentication/validate-tokens/>
191 #[cfg(feature = "client")]
192 pub async fn validate_token<'a, C>(
193 &self,
194 client: &'a C,
195 ) -> Result<ValidatedToken, ValidationError<<C as Client>::Error>>
196 where
197 C: Client,
198 {
199 let req = self.validate_token_request();
200
201 let resp = client.req(req).await.map_err(ValidationError::Request)?;
202 if resp.status() == StatusCode::UNAUTHORIZED {
203 return Err(ValidationError::NotAuthorized);
204 }
205 ValidatedToken::from_response(&resp).map_err(|v| v.into_other())
206 }
207
208 /// Get the request needed to revoke this token.
209 pub fn revoke_token_request(&self, client_id: &ClientId) -> http::Request<Vec<u8>> {
210 use http::{HeaderMap, Method};
211 use std::collections::HashMap;
212 let mut params = HashMap::new();
213 params.insert("client_id", client_id.as_str());
214 params.insert("token", self.secret());
215
216 construct_request(
217 &crate::REVOKE_URL,
218 ¶ms,
219 HeaderMap::new(),
220 Method::POST,
221 vec![],
222 )
223 }
224
225 /// Revoke the token.
226 ///
227 /// See <https://dev.twitch.tv/docs/authentication/revoke-tokens/>
228 #[cfg(feature = "client")]
229 pub async fn revoke_token<'a, C>(
230 &self,
231 http_client: &'a C,
232 client_id: &ClientId,
233 ) -> Result<(), RevokeTokenError<<C as Client>::Error>>
234 where
235 C: Client,
236 {
237 let req = self.revoke_token_request(client_id);
238
239 let resp = http_client
240 .req(req)
241 .await
242 .map_err(RevokeTokenError::RequestError)?;
243
244 let _ = parse_token_response_raw(&resp)?;
245 Ok(())
246 }
247}
248
249impl RefreshTokenRef {
250 /// Get the request needed to refresh this token.
251 ///
252 /// Parse the response from this endpoint with [TwitchTokenResponse::from_response](crate::id::TwitchTokenResponse::from_response)
253 pub fn refresh_token_request(
254 &self,
255 client_id: &ClientId,
256 client_secret: Option<&ClientSecret>,
257 ) -> http::Request<Vec<u8>> {
258 use http::{HeaderMap, Method};
259 use std::collections::HashMap;
260
261 let mut params = HashMap::new();
262 params.insert("client_id", client_id.as_str());
263 if let Some(client_secret) = client_secret {
264 params.insert("client_secret", client_secret.secret());
265 }
266 params.insert("grant_type", "refresh_token");
267 params.insert("refresh_token", self.secret());
268
269 construct_request(
270 &crate::TOKEN_URL,
271 ¶ms,
272 HeaderMap::new(),
273 Method::POST,
274 vec![],
275 )
276 }
277
278 /// Refresh the token, call if it has expired.
279 ///
280 /// See <https://dev.twitch.tv/docs/authentication/refresh-tokens>
281 #[cfg(feature = "client")]
282 pub async fn refresh_token<'a, C>(
283 &self,
284 http_client: &'a C,
285 client_id: &ClientId,
286 client_secret: Option<&ClientSecret>,
287 ) -> Result<
288 (AccessToken, std::time::Duration, Option<RefreshToken>),
289 RefreshTokenError<<C as Client>::Error>,
290 >
291 where
292 C: Client,
293 {
294 let req = self.refresh_token_request(client_id, client_secret);
295
296 let resp = http_client
297 .req(req)
298 .await
299 .map_err(RefreshTokenError::RequestError)?;
300 let res = id::TwitchTokenResponse::from_response(&resp)?;
301
302 let expires_in = res.expires_in().ok_or(RefreshTokenError::NoExpiration)?;
303 let refresh_token = res.refresh_token;
304 let access_token = res.access_token;
305 Ok((access_token, expires_in, refresh_token))
306 }
307}
308
309/// Construct a request that accepts `application/json` on default
310fn construct_request<I, K, V>(
311 url: &url::Url,
312 params: I,
313 headers: http::HeaderMap,
314 method: http::Method,
315 body: Vec<u8>,
316) -> http::Request<Vec<u8>>
317where
318 I: std::iter::IntoIterator,
319 I::Item: std::borrow::Borrow<(K, V)>,
320 K: AsRef<str>,
321 V: AsRef<str>,
322{
323 let mut url = url.clone();
324 url.query_pairs_mut().extend_pairs(params);
325 let url: String = url.into();
326 let mut req = http::Request::builder().method(method).uri(url);
327 req.headers_mut().map(|h| h.extend(headers)).unwrap();
328 req.headers_mut()
329 .map(|h| {
330 if !h.contains_key(http::header::ACCEPT) {
331 h.append(http::header::ACCEPT, "application/json".parse().unwrap());
332 }
333 })
334 .unwrap();
335 req.body(body).unwrap()
336}
337
338/// Parses a response, validating it and returning the response if all ok.
339pub(crate) fn parse_token_response_raw<B: AsRef<[u8]>>(
340 resp: &http::Response<B>,
341) -> Result<&http::Response<B>, RequestParseError> {
342 match serde_json::from_slice::<TwitchTokenErrorResponse>(resp.body().as_ref()) {
343 Err(_) => match resp.status() {
344 StatusCode::OK => Ok(resp),
345 _ => Err(RequestParseError::Other(resp.status())),
346 },
347 Ok(twitch_err) => Err(RequestParseError::TwitchError(twitch_err)),
348 }
349}
350
351/// Parses a response, validating it and returning json deserialized response
352pub(crate) fn parse_response<T: serde::de::DeserializeOwned, B: AsRef<[u8]>>(
353 resp: &http::Response<B>,
354) -> Result<T, RequestParseError> {
355 let body = parse_token_response_raw(resp)?.body().as_ref();
356 if let Some(_content) = resp.headers().get(http::header::CONTENT_TYPE) {
357 // TODO: Remove this cfg, see issue https://github.com/twitchdev/twitch-cli/issues/81
358 #[cfg(not(feature = "mock_api"))]
359 if _content != "application/json" {
360 return Err(RequestParseError::NotJson {
361 found: String::from_utf8_lossy(_content.as_bytes()).into_owned(),
362 });
363 }
364 }
365 serde_json::from_slice(body).map_err(Into::into)
366}
367
368/// Errors from parsing responses
369#[derive(Debug, thiserror::Error, displaydoc::Display)]
370#[non_exhaustive]
371pub enum RequestParseError {
372 /// deserialization failed
373 DeserializeError(#[from] serde_json::Error),
374 /// twitch returned an error
375 TwitchError(#[from] TwitchTokenErrorResponse),
376 /// returned content is not `application/json`, found `{found}`
377 NotJson {
378 /// Found `Content-Type` header
379 found: String,
380 },
381 /// twitch returned an unexpected status code: {0}
382 Other(StatusCode),
383}