Skip to main content

twitch_oauth2/
tokens.rs

1//! Twitch token types
2
3mod app_access_token;
4pub mod errors;
5mod user_token;
6
7#[cfg(feature = "client")]
8use std::future::Future;
9
10pub use app_access_token::AppAccessToken;
11use twitch_types::{UserId, UserIdRef, UserName, UserNameRef};
12pub use user_token::{
13    DeviceUserTokenBuilder, ImplicitUserTokenBuilder, UserToken, UserTokenBuilder,
14};
15
16#[cfg(feature = "client")]
17use crate::client::Client;
18use crate::{id::TwitchTokenErrorResponse, scopes::Scope, RequestParseError};
19
20use errors::ValidationError;
21#[cfg(feature = "client")]
22use errors::{RefreshTokenError, RevokeTokenError};
23
24use crate::types::{AccessToken, ClientId};
25use serde_derive::Deserialize;
26
27#[derive(Clone, Debug, PartialEq, Eq)]
28/// Types of bearer tokens
29pub enum BearerTokenType {
30    /// Token for making requests in the context of an authenticated user.
31    UserToken,
32    /// Token for server-to-server requests.
33    ///
34    /// In some contexts (i.e [EventSub](https://dev.twitch.tv/docs/eventsub)) an App Access Token can be used in the context of users that have authenticated
35    /// the specific Client ID
36    AppAccessToken,
37}
38
39/// Trait for twitch tokens to get fields and generalize over [AppAccessToken] and [UserToken]
40pub trait TwitchToken {
41    /// Get the type of token.
42    fn token_type() -> BearerTokenType;
43    /// Client ID associated with the token. Twitch requires this in all helix API calls
44    fn client_id(&self) -> &ClientId;
45    /// Get the [AccessToken] for authenticating
46    ///
47    /// # Example
48    ///
49    /// ```rust, no_run
50    /// # use twitch_oauth2::UserToken;
51    /// # fn t() -> UserToken {todo!()}
52    /// # let user_token = t();
53    /// use twitch_oauth2::TwitchToken;
54    /// println!("token: {}", user_token.token().secret());
55    /// ```
56    fn token(&self) -> &AccessToken;
57    /// Get the username associated to this token
58    fn login(&self) -> Option<&UserNameRef>;
59    /// Get the user id associated to this token
60    fn user_id(&self) -> Option<&UserIdRef>;
61    /// Refresh this token, changing the token to a newer one
62    #[cfg(feature = "client")]
63    fn refresh_token<'a, C>(
64        &mut self,
65        http_client: &'a C,
66    ) -> impl Future<Output = Result<(), RefreshTokenError<<C as Client>::Error>>> + Send
67    where
68        Self: Sized,
69        C: Client;
70    /// Get current lifetime of token.
71    fn expires_in(&self) -> std::time::Duration;
72
73    /// Returns whether or not the token is expired.
74    ///
75    /// ```rust, no_run
76    /// # use twitch_oauth2::UserToken;
77    /// # fn t() -> UserToken {todo!()}
78    /// # #[tokio::main]
79    /// # async fn run() -> Result<(), Box<dyn std::error::Error + 'static>>{
80    /// # let mut user_token = t();
81    /// use twitch_oauth2::{UserToken, TwitchToken};
82    /// if user_token.is_elapsed() {
83    ///     user_token.refresh_token(&reqwest::Client::builder().redirect(reqwest::redirect::Policy::none()).build()?).await?;
84    /// }
85    /// # Ok(()) }
86    /// # fn main() {run();}
87    fn is_elapsed(&self) -> bool {
88        let exp = self.expires_in();
89        exp.as_secs() == 0 && exp.as_nanos() == 0
90    }
91    /// Retrieve scopes attached to the token
92    fn scopes(&self) -> &[Scope];
93    /// Validate this token. Should be checked on regularly, according to <https://dev.twitch.tv/docs/authentication/validate-tokens/>
94    ///
95    /// # Note
96    ///
97    /// This will not mutate any current data in the [TwitchToken]
98    #[cfg(feature = "client")]
99    fn validate_token<'a, C>(
100        &self,
101        http_client: &'a C,
102    ) -> impl Future<Output = Result<ValidatedToken, ValidationError<<C as Client>::Error>>> + Send
103    where
104        Self: Sized,
105        C: Client,
106    {
107        let token = &self.token();
108        token.validate_token(http_client)
109    }
110
111    /// Revoke the token. See <https://dev.twitch.tv/docs/authentication/revoke-tokens>
112    #[cfg(feature = "client")]
113    fn revoke_token<'a, C>(
114        self,
115        http_client: &'a C,
116    ) -> impl Future<Output = Result<(), RevokeTokenError<<C as Client>::Error>>> + Send
117    where
118        Self: Sized + Send,
119        C: Client,
120    {
121        async move {
122            let token = self.token();
123            let client_id = self.client_id();
124            token.revoke_token(http_client, client_id).await
125        }
126    }
127}
128
129impl<T: TwitchToken + Send> TwitchToken for Box<T> {
130    fn token_type() -> BearerTokenType { T::token_type() }
131
132    fn client_id(&self) -> &ClientId { (**self).client_id() }
133
134    fn token(&self) -> &AccessToken { (**self).token() }
135
136    fn login(&self) -> Option<&UserNameRef> { (**self).login() }
137
138    fn user_id(&self) -> Option<&UserIdRef> { (**self).user_id() }
139
140    #[cfg(feature = "client")]
141    async fn refresh_token<'a, C>(
142        &mut self,
143        http_client: &'a C,
144    ) -> Result<(), RefreshTokenError<<C as Client>::Error>>
145    where
146        Self: Sized,
147        C: Client,
148    {
149        (**self).refresh_token(http_client).await
150    }
151
152    fn expires_in(&self) -> std::time::Duration { (**self).expires_in() }
153
154    fn scopes(&self) -> &[Scope] { (**self).scopes() }
155}
156
157/// Token validation returned from `https://id.twitch.tv/oauth2/validate`
158///
159/// See <https://dev.twitch.tv/docs/authentication/validate-tokens/>
160#[derive(Debug, Clone, Deserialize)]
161pub struct ValidatedToken {
162    /// Client ID associated with the token. Twitch requires this in all helix API calls
163    pub client_id: ClientId,
164    /// Username associated with the token
165    pub login: Option<UserName>,
166    /// User ID associated with the token
167    pub user_id: Option<UserId>,
168    /// Scopes attached to the token.
169    pub scopes: Option<Vec<Scope>>,
170    /// Lifetime of the token
171    #[serde(deserialize_with = "expires_in")]
172    pub expires_in: Option<std::time::Duration>,
173}
174
175fn expires_in<'a, D: serde::de::Deserializer<'a>>(
176    d: D,
177) -> Result<Option<std::time::Duration>, D::Error> {
178    use serde::Deserialize;
179    let num = u64::deserialize(d)?;
180    if num == 0 {
181        Ok(None)
182    } else {
183        Ok(Some(std::time::Duration::from_secs(num)))
184    }
185}
186
187impl ValidatedToken {
188    /// Assemble a a validated token from a response.
189    ///
190    /// Get the request that generates this response with [`AccessToken::validate_token_request`][crate::types::AccessTokenRef::validate_token_request]
191    pub fn from_response<B: AsRef<[u8]>>(
192        response: &http::Response<B>,
193    ) -> Result<ValidatedToken, ValidationError<std::convert::Infallible>> {
194        match crate::parse_response(response) {
195            Ok(ok) => Ok(ok),
196            Err(err) => match err {
197                RequestParseError::TwitchError(TwitchTokenErrorResponse { status, .. })
198                    if status == http::StatusCode::UNAUTHORIZED =>
199                {
200                    Err(ValidationError::NotAuthorized)
201                }
202                err => Err(err.into()),
203            },
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use crate::ValidatedToken;
211
212    use super::errors::ValidationError;
213
214    #[test]
215    fn validated_token() {
216        let body = br#"
217        {
218            "client_id": "wbmytr93xzw8zbg0p1izqyzzc5mbiz",
219            "login": "twitchdev",
220            "scopes": [
221              "channel:read:subscriptions"
222            ],
223            "user_id": "141981764",
224            "expires_in": 5520838
225        }
226        "#;
227        let response = http::Response::builder().status(200).body(body).unwrap();
228        ValidatedToken::from_response(&response).unwrap();
229    }
230
231    #[test]
232    fn validated_non_expiring_token() {
233        let body = br#"
234        {
235            "client_id": "wbmytr93xzw8zbg0p1izqyzzc5mbiz",
236            "login": "twitchdev",
237            "scopes": [
238              "channel:read:subscriptions"
239            ],
240            "user_id": "141981764",
241            "expires_in": 0
242        }
243        "#;
244        let response = http::Response::builder().status(200).body(body).unwrap();
245        let token = ValidatedToken::from_response(&response).unwrap();
246        assert!(token.expires_in.is_none());
247    }
248
249    #[test]
250    fn validated_error_response() {
251        let body = br#"
252        {
253            "status": 401,
254            "message": "missing authorization token",
255        }
256        "#;
257        let response = http::Response::builder().status(401).body(body).unwrap();
258        let error = ValidatedToken::from_response(&response).unwrap_err();
259        assert!(matches!(error, ValidationError::RequestParseError(_)))
260    }
261}