twitch_api_rs/auth/
mod.rs

1//! Types and functions for auth flows
2//!
3//! See [`Twitch Auth Documentation`]
4//!
5//! [`Twitch Auth Documentation`]: https://dev.twitch.tv/docs/authentication
6
7pub mod scopes;
8
9/// Represents a authorization token of some type that can be sent as a header to a
10/// twitch endpoint.
11pub trait AuthToken: crate::requests::Headers + Clone {
12    /// Get the set of scopes that this token has associated with it
13    fn scopes(&self) -> &scopes::ScopeSet;
14}
15
16use reqwest::RequestBuilder;
17use std::rc::Rc;
18use std::sync::Arc;
19
20impl<H> crate::requests::Headers for Arc<H>
21where
22    H: crate::requests::Headers,
23{
24    fn write_headers(&self, req: RequestBuilder) -> RequestBuilder {
25        self.as_ref().write_headers(req)
26    }
27}
28
29impl<A> AuthToken for Arc<A>
30where
31    A: AuthToken,
32{
33    fn scopes(&self) -> &scopes::ScopeSet {
34        self.as_ref().scopes()
35    }
36}
37
38impl<H> crate::requests::Headers for Rc<H>
39where
40    H: crate::requests::Headers,
41{
42    fn write_headers(&self, req: RequestBuilder) -> RequestBuilder {
43        self.as_ref().write_headers(req)
44    }
45}
46
47impl<A> AuthToken for Rc<A>
48where
49    A: AuthToken,
50{
51    fn scopes(&self) -> &scopes::ScopeSet {
52        self.as_ref().scopes()
53    }
54}
55
56use crate::values::FieldValue;
57use crate::{field_wrapper_name, from_inner, quick_deref_into};
58use serde::{Deserialize, Serialize};
59
60#[repr(transparent)]
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(transparent)]
63/// Represents a [`crate::auth::client_credentials`] id.  
64/// See [`Twitch Auth Guide`] for more
65///
66/// [`Twitch Auth Guide`]: https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-client-credentials-flow
67pub struct ClientId(String);
68
69#[repr(transparent)]
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(transparent)]
72/// Represents a [`crate::auth::client_credentials`] secret.  
73/// See [`Twitch Auth Guide`] for more
74///
75/// [`Twitch Auth Guide`]: https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-client-credentials-flow
76pub struct ClientSecret(String);
77
78quick_deref_into![(ClientId, String), (ClientSecret, String)];
79from_inner![(ClientId, String), (ClientSecret, String)];
80field_wrapper_name![ClientId => "client_id", ClientSecret => "client_secret"];
81
82/// [`Implicit Code`] Flow
83///
84/// [`Implicit Code`]: https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#oauth-implicit-code-flow
85pub mod implicit_code {}
86
87/// [`Authorization Code`] Flow
88///
89/// [`Authorization Code`]: https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#oauth-implicit-code-flow
90pub mod authorization_code {}
91
92/// [`Client Credentials`] Flow
93///
94/// [`Client Credentials`]: https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#oauth-client-credentials-flow
95///
96/// ```ignore
97/// # use twitch_api_rs::requests::*;
98/// # let (client_id, client_secret) = (
99/// #     String::from("uo6dggojyb8d6soh92zknwmi5ej1q2"),
100/// #     String::from("nyo51xcdrerl8z9m56w9w6wg"),
101/// # );
102///
103/// let client_resp = ClientAuthRequestBuilder::builder()
104///     .set_client_id(client_id)
105///     .set_client_secret(client_secret)
106///     .make_request()
107///     .await;
108///
109/// match client_resp {
110///
111///     Ok(resp) => {
112///         let (token, expiration) = resp.into();
113///         eprintln!("Got Token {}. (Expires in {} seconds)", token, expiration);
114///     }
115///
116///     Err(RequestError::MalformedRequest(msg)) =>
117///         unreachable!("We set all the parameters but the struct said {}", msg),
118///
119///     Err(RequestError::ErrorCodes(code)) =>
120///         eprintln!("Server rejected request for reason {}", code),
121///
122///     Err(e) =>
123///         eprintln!("Failed to make request for reason {}", e),
124///
125/// }
126/// ```
127pub mod client_credentials {
128
129    use super::*;
130    use crate::requests::*; // TODO: Replace with internal prelude
131    use reqwest::RequestBuilder;
132    use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer};
133
134    #[derive(Debug)]
135    #[doc(hidden)]
136    /// Do not use directly, instead use [`ClientAuthRequest`]
137    pub struct ClientAuthRequestParams {
138        client_id: Option<ClientId>,
139        client_secret: Option<ClientSecret>,
140        scopes: Vec<String>, // TODO change to list of Scope Enum items or maybe bitset that has display trait and named bits
141    }
142
143    impl ParametersExt for ClientAuthRequestParams {}
144
145    #[derive(Debug)]
146    /// Request for the [`client authentication`] flow.  
147    /// See module level documentation for usage.
148    ///
149    /// implemnts [`Request`], see documentation for more information.
150    ///
151    /// [`client authentication`]: https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#oauth-client-credentials-flow
152    pub struct ClientAuthRequest {
153        params: ClientAuthRequestParams,
154    }
155
156    impl Serialize for ClientAuthRequestParams {
157        fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
158        where
159            S: Serializer,
160        {
161            let mut map = ser.serialize_map(Some(if self.scopes.len() > 0 { 4 } else { 3 }))?;
162            map.serialize_entry("client_id", self.client_id.as_ref().unwrap())?;
163            map.serialize_entry("client_secret", self.client_secret.as_ref().unwrap())?;
164            map.serialize_entry("grant_type", "client_credentials")?;
165
166            // TODO Serialize vec as space separated list
167
168            map.end()
169        }
170    }
171
172    #[cfg_attr(feature = "nightly", doc(spotlight))]
173    impl Request for ClientAuthRequest {
174        const ENDPOINT: &'static str = "https://id.twitch.tv/oauth2/token";
175
176        type Headers = ();
177        type Parameters = ClientAuthRequestParams;
178        type Body = ();
179
180        type Response = ClientAuthResponse;
181
182        type ErrorCodes = CommonResponseCodes;
183
184        const METHOD: reqwest::Method = reqwest::Method::POST;
185
186        fn builder() -> Self {
187            Self {
188                params: ClientAuthRequestParams {
189                    client_id: None,
190                    client_secret: None,
191                    scopes: Vec::new(),
192                },
193            }
194        }
195
196        fn headers(&self) -> &Self::Headers {
197            &()
198        }
199        fn parameters(&self) -> &Self::Parameters {
200            &self.params
201        }
202        fn body(&self) -> &Self::Body {
203            &()
204        }
205
206        fn ready(&self) -> Result<(), RequestError<Self::ErrorCodes>> {
207            if self.params.client_id.is_none() {
208                Err(RequestError::MalformedRequest(String::from(
209                    "field client_id must be set",
210                )))
211            } else if self.params.client_secret.is_none() {
212                Err(RequestError::MalformedRequest(String::from(
213                    "field client_secret must be set",
214                )))
215            } else {
216                Ok(())
217            }
218        }
219    }
220
221    impl ClientAuthRequest {
222        /// Set the client_id
223        pub fn set_client_id<I: Into<ClientId>>(&mut self, client_id: I) -> &mut Self {
224            self.params.client_id.replace(client_id.into());
225            self
226        }
227
228        /// Set the client_secret
229        pub fn set_client_secret<S: Into<ClientSecret>>(&mut self, client_secret: S) -> &mut Self {
230            self.params.client_secret.replace(client_secret.into());
231            self
232        }
233    }
234
235    /// Build a complete request from `(client_id, client_secret)`
236    impl<I, S> From<(I, S)> for ClientAuthRequest
237    where
238        I: Into<ClientId>,
239        S: Into<ClientSecret>,
240    {
241        fn from((client_id, client_secret): (I, S)) -> Self {
242            Self {
243                params: ClientAuthRequestParams {
244                    client_id: Some(client_id.into()),
245                    client_secret: Some(client_secret.into()),
246                    scopes: vec![],
247                },
248            }
249        }
250    }
251
252    #[derive(Debug, Deserialize)]
253    /// Response from a successful [`ClientAuthRequest`]
254    ///
255    /// See module level docuemntation to see how to get
256    pub struct ClientAuthResponse {
257        /// The access_token returned by twitch
258        pub access_token: String,
259        // refresh_token:
260        /// The amount of seconds until the token expires
261        pub expires_in: u32,
262        // token_type: String // Always bearer
263    }
264
265    impl Into<(String, u32)> for ClientAuthResponse {
266        fn into(self) -> (String, u32) {
267            (self.access_token, self.expires_in)
268        }
269    }
270
271    use super::scopes::ScopeSet;
272
273    /// Represents an authorization token header for requests
274    #[derive(Debug, Clone)]
275    #[allow(missing_docs)]
276    pub struct ClientAuthToken {
277        scopes: ScopeSet,
278        pub token: String,
279        pub client_id: ClientId,
280    }
281
282    impl ClientAuthToken {
283        /// Create the auth token from a sucessful auth response and a client_id
284        pub fn from_client<C>(auth_response: ClientAuthResponse, client_id: C) -> Self
285        where
286            C: Into<ClientId>,
287        {
288            Self {
289                // Fill with empty scopes item as scopes only apply to OAuth tokens
290                scopes: ScopeSet::new(),
291                token: auth_response.access_token,
292                client_id: client_id.into(),
293            }
294        }
295    }
296
297    impl Headers for ClientAuthToken {
298        fn write_headers(&self, req: RequestBuilder) -> RequestBuilder {
299            req.header("Authorization", format!("Bearer {}", self.token))
300                .header("Client-Id", std::ops::Deref::deref(&self.client_id))
301        }
302    }
303
304    impl super::AuthToken for ClientAuthToken {
305        fn scopes(&self) -> &ScopeSet {
306            &self.scopes
307        }
308    }
309}