twitch_oauth2/
tokens.rs

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