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