Skip to main content

twitch_oauth2/tokens/
app_access_token.rs

1use twitch_types::{UserIdRef, 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::{AppAccessTokenError, ValidationError};
11#[cfg(feature = "client")]
12use crate::client::Client;
13#[cfg(feature = "client")]
14use crate::tokens::errors::RefreshTokenError;
15use crate::tokens::{Scope, TwitchToken};
16use crate::{
17    types::{AccessToken, ClientId, ClientSecret, RefreshToken},
18    ClientIdRef, ClientSecretRef,
19};
20
21/// An App Access Token from the [OAuth client credentials flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#client-credentials-grant-flow)
22///
23/// Used for server-to-server requests. Use [`UserToken`](super::UserToken) for requests that need to be in the context of an authenticated user.
24///
25/// 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
26/// the specific Client ID
27#[derive(Clone)]
28pub struct AppAccessToken {
29    /// The access token used to authenticate requests with
30    pub access_token: AccessToken,
31    /// The refresh token used to extend the life of this user token
32    pub refresh_token: Option<RefreshToken>,
33    /// Expiration from when the response was generated.
34    expires_in: std::time::Duration,
35    /// When this struct was created, not when token was created.
36    struct_created: Instant,
37    client_id: ClientId,
38    client_secret: ClientSecret,
39    scopes: Vec<Scope>,
40}
41
42impl std::fmt::Debug for AppAccessToken {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        f.debug_struct("AppAccessToken")
45            .field("access_token", &self.access_token)
46            .field("refresh_token", &self.refresh_token)
47            .field("client_id", &self.client_id)
48            .field("client_secret", &self.client_secret)
49            .field("expires_in", &self.expires_in())
50            .field("scopes", &self.scopes)
51            .finish()
52    }
53}
54
55impl TwitchToken for AppAccessToken {
56    fn token_type() -> super::BearerTokenType { super::BearerTokenType::AppAccessToken }
57
58    fn client_id(&self) -> &ClientId { &self.client_id }
59
60    fn token(&self) -> &AccessToken { &self.access_token }
61
62    fn login(&self) -> Option<&UserNameRef> { None }
63
64    fn user_id(&self) -> Option<&UserIdRef> { None }
65
66    #[cfg(feature = "client")]
67    async fn refresh_token<'a, C>(
68        &mut self,
69        http_client: &'a C,
70    ) -> Result<(), RefreshTokenError<<C as Client>::Error>>
71    where
72        C: Client,
73    {
74        let (access_token, expires_in, refresh_token) =
75            if let Some(token) = self.refresh_token.take() {
76                token
77                    .refresh_token(http_client, &self.client_id, Some(&self.client_secret))
78                    .await?
79            } else {
80                return Err(RefreshTokenError::NoRefreshToken);
81            };
82        self.access_token = access_token;
83        self.expires_in = expires_in;
84        self.refresh_token = refresh_token;
85        self.struct_created = Instant::now();
86        Ok(())
87    }
88
89    fn expires_in(&self) -> std::time::Duration {
90        self.expires_in
91            .checked_sub(self.struct_created.elapsed())
92            .unwrap_or_default()
93    }
94
95    fn scopes(&self) -> &[Scope] { self.scopes.as_slice() }
96}
97
98impl AppAccessToken {
99    /// Assemble token without checks.
100    ///
101    /// This is useful if you already have an app access token and want to use it with this library. Be careful however,
102    /// as this function does not check if the token is valid or expired, nor if it is an `app access token` or `user token`.
103    ///
104    /// # Notes
105    ///
106    /// If `expires_in` is `None`, we'll assume `token.is_elapsed() == true`
107    pub fn from_existing_unchecked(
108        access_token: AccessToken,
109        refresh_token: impl Into<Option<RefreshToken>>,
110        client_id: impl Into<ClientId>,
111        client_secret: impl Into<ClientSecret>,
112        scopes: Option<Vec<Scope>>,
113        expires_in: Option<std::time::Duration>,
114    ) -> AppAccessToken {
115        AppAccessToken {
116            access_token,
117            refresh_token: refresh_token.into(),
118            client_id: client_id.into(),
119            client_secret: client_secret.into(),
120            expires_in: expires_in.unwrap_or_default(),
121            struct_created: Instant::now(),
122            scopes: scopes.unwrap_or_default(),
123        }
124    }
125
126    /// Assemble token and validate it. Retrieves [`client_id`](TwitchToken::client_id) and [`scopes`](TwitchToken::scopes).
127    #[cfg(feature = "client")]
128    pub async fn from_existing<C>(
129        http_client: &C,
130        access_token: AccessToken,
131        refresh_token: impl Into<Option<RefreshToken>>,
132        client_secret: ClientSecret,
133    ) -> Result<AppAccessToken, ValidationError<<C as Client>::Error>>
134    where
135        C: Client,
136    {
137        let token = access_token;
138        let validated = token.validate_token(http_client).await?;
139        if validated.user_id.is_some() {
140            return Err(ValidationError::InvalidToken(
141                "expected an app access token, got a user access token",
142            ));
143        }
144        Ok(Self::from_existing_unchecked(
145            token,
146            refresh_token.into(),
147            validated.client_id,
148            client_secret,
149            validated.scopes,
150            validated.expires_in,
151        ))
152    }
153
154    /// Assemble token from twitch responses.
155    pub fn from_response(
156        response: crate::id::TwitchTokenResponse,
157        client_id: impl Into<ClientId>,
158        client_secret: impl Into<ClientSecret>,
159    ) -> AppAccessToken {
160        let expires_in = response.expires_in();
161        AppAccessToken::from_existing_unchecked(
162            response.access_token,
163            response.refresh_token,
164            client_id.into(),
165            client_secret,
166            response.scopes,
167            expires_in,
168        )
169    }
170
171    /// Generate an app access token via [OAuth client credentials flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#client-credentials-grant-flow)
172    ///
173    /// # Examples
174    ///
175    /// ```rust,no_run
176    /// use twitch_oauth2::{AccessToken, AppAccessToken};
177    /// // Make sure you enable the feature "reqwest" for twitch_oauth2 if you want to use reqwest
178    /// # async {let client = twitch_oauth2::client::DummyClient; stringify!(
179    /// let client = reqwest::Client::builder()
180    ///     .redirect(reqwest::redirect::Policy::none())
181    ///     .build()?;
182    /// # );
183    /// let token = AppAccessToken::get_app_access_token(
184    ///     &client,
185    ///     "my_client_id".into(),
186    ///     "my_client_secret".into(),
187    ///     vec![], // scopes
188    /// )
189    /// .await?;
190    /// # Ok::<(), Box<dyn std::error::Error>>(())};
191    /// ```
192    #[cfg(feature = "client")]
193    pub async fn get_app_access_token<C>(
194        http_client: &C,
195        client_id: ClientId,
196        client_secret: ClientSecret,
197        scopes: Vec<Scope>,
198    ) -> Result<AppAccessToken, AppAccessTokenError<<C as Client>::Error>>
199    where
200        C: Client,
201    {
202        let req = Self::get_app_access_token_request(&client_id, &client_secret, scopes);
203
204        let resp = http_client
205            .req(req)
206            .await
207            .map_err(AppAccessTokenError::Request)?;
208
209        let response = crate::id::TwitchTokenResponse::from_response(&resp)?;
210        let app_access = AppAccessToken::from_response(response, client_id, client_secret);
211
212        Ok(app_access)
213    }
214
215    /// Get the request for getting an app access token.
216    ///
217    /// Parse with [TwitchTokenResponse::from_response](crate::id::TwitchTokenResponse::from_response) and [AppAccessToken::from_response]
218    pub fn get_app_access_token_request(
219        client_id: &ClientIdRef,
220        client_secret: &ClientSecretRef,
221        scopes: Vec<Scope>,
222    ) -> http::Request<Vec<u8>> {
223        use http::{HeaderMap, Method};
224        use std::collections::HashMap;
225        let scope: String = scopes
226            .iter()
227            .map(|s| s.to_string())
228            .collect::<Vec<_>>()
229            .join(" ");
230        let mut params = HashMap::new();
231        params.insert("client_id", client_id.as_str());
232        params.insert("client_secret", client_secret.secret());
233        params.insert("grant_type", "client_credentials");
234        params.insert("scope", &scope);
235
236        crate::construct_request(
237            &crate::TOKEN_URL,
238            &params,
239            HeaderMap::new(),
240            Method::POST,
241            vec![],
242        )
243    }
244}