twitch_oauth2/tokens/
app_access_token.rs

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