spotify_rs/
auth.rs

1use std::{collections::HashSet, fmt::Debug, time::Duration};
2
3use chrono::{DateTime, Utc};
4use oauth2::{
5    basic::BasicTokenType, AccessToken, CsrfToken, PkceCodeVerifier, RefreshToken, TokenResponse,
6};
7use serde::{Deserialize, Serialize};
8
9// Typestate trait definitions and implementations.
10pub trait AuthenticationState: private::Sealed {}
11impl AuthenticationState for Token {}
12impl AuthenticationState for Unauthenticated {}
13
14pub trait AuthFlow: private::Sealed + Debug {}
15impl AuthFlow for AuthCodeFlow {}
16impl AuthFlow for AuthCodePkceFlow {}
17impl AuthFlow for ClientCredsFlow {}
18impl AuthFlow for UnknownFlow {}
19
20impl Debug for AuthCodeFlow {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        f.debug_struct("AuthCodeFlow")
23            .field("csrf_token", &"[redacted]")
24            .finish()
25    }
26}
27
28impl Debug for AuthCodePkceFlow {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        f.debug_struct("AuthCodePkceFlow")
31            .field("csrf_token", &"[redacted]")
32            .field("pkce_verifier", &"[redacted]")
33            .finish()
34    }
35}
36
37pub trait Authorised: private::Sealed {}
38impl Authorised for AuthCodeFlow {}
39impl Authorised for AuthCodePkceFlow {}
40
41// The only way to have an unknown flow is by creating the client from a
42// refresh token, which is only available for authorised flows, thus this
43// is authorised.
44impl Authorised for UnknownFlow {}
45
46// Make it so users of the crate can't implement the typestate traits for their
47// own types (which might not work anyway).
48mod private {
49    pub trait Sealed {}
50
51    impl Sealed for super::Token {}
52    impl Sealed for super::Unauthenticated {}
53    impl Sealed for super::AuthCodeFlow {}
54    impl Sealed for super::AuthCodePkceFlow {}
55    impl Sealed for super::ClientCredsFlow {}
56    impl Sealed for super::UnknownFlow {}
57}
58
59/// A list of (unique) scopes. You don't usually have to interact
60/// with it directly, the conversion should happen implicitly, with the exception of
61/// the [`from_refresh_token`](Client::from_refresh_token) function.
62///
63/// In such cases, you should just call [into](Into::into) on your list of scopes.
64#[derive(Clone, Debug, Default)]
65pub struct Scopes(pub(crate) HashSet<oauth2::Scope>);
66
67impl<I> From<I> for Scopes
68where
69    I: IntoIterator,
70    I::Item: Into<String>,
71{
72    fn from(value: I) -> Self {
73        let scopes = value
74            .into_iter()
75            .map(|i| oauth2::Scope::new(i.into()))
76            .collect();
77        Self(scopes)
78    }
79}
80
81impl Scopes {
82    /// Create a list of scopes for an iterator of items implement Into<String>.
83    ///
84    /// This is just using the existing From implementation, but exists to make
85    /// it clearer how to create the struct for users.
86    pub fn new<I>(scopes: I) -> Self
87    where
88        I: IntoIterator,
89        I::Item: Into<String>,
90    {
91        Self::from(scopes)
92    }
93
94    fn inner_vec(self) -> Vec<oauth2::Scope> {
95        self.0.into_iter().collect()
96    }
97}
98
99/// An OAuth2 token.
100#[derive(Clone, Debug, Deserialize, Serialize)]
101pub struct Token {
102    /// The token used for authenticating every single request.
103    pub(crate) access_token: AccessToken,
104    /// The token used for requesting a new access token when the current one expires.
105    pub(crate) refresh_token: Option<RefreshToken>,
106    /// How long until the current token expires, in seconds.
107    pub expires_in: u64,
108
109    #[serde(default = "Utc::now")]
110    /// The UTC date and time when the token was created.
111    pub created_at: DateTime<Utc>,
112
113    #[serde(skip)]
114    /// The UTC date and time when the token will expire.
115    pub expires_at: DateTime<Utc>,
116
117    #[serde(deserialize_with = "oauth2::helpers::deserialize_untagged_enum_case_insensitive")]
118    pub(crate) token_type: BasicTokenType,
119    #[serde(rename = "scope")]
120    #[serde(deserialize_with = "oauth2::helpers::deserialize_space_delimited_vec")]
121    #[serde(serialize_with = "oauth2::helpers::serialize_space_delimited_vec")]
122    #[serde(skip_serializing_if = "Option::is_none")]
123    #[serde(default)]
124    pub(crate) scopes: Option<Vec<oauth2::Scope>>,
125}
126
127// Represents the state of a client that's not authenticated.
128#[doc = include_str!("docs/internal_implementation_details.md")]
129#[derive(Clone, Copy, Debug)]
130pub struct Unauthenticated;
131
132/// Represents the [Authorisation Code Flow](https://developer.spotify.com/documentation/web-api/tutorials/code-flow),
133/// as defined in [OAuth2 RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1).
134///
135/// Its use is recommended in cases where the `client_secret` can be safely stored,
136/// such as a web app running on the server.
137///
138/// This flow requires user authorisation, and thus allows the app
139/// to make requests on behalf of the user.
140pub struct AuthCodeFlow {
141    pub(crate) csrf_token: CsrfToken,
142}
143
144/// Represents the [Authorisation Code Flow with PKCE extension](https://developer.spotify.com/documentation/web-api/tutorials/code-pkce-flow),
145/// as defined in [OAuth2 RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) and [OAuth2 RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636).
146///
147/// Its use is recommended in cases where storing the `client_secret` safely is
148/// *not* possible, such as web apps running on the client, desktop and mobile apps.
149///
150/// This flow requires user authorisation, and thus allows the app
151/// to make requests on behalf of the user.
152pub struct AuthCodePkceFlow {
153    pub(crate) csrf_token: CsrfToken,
154    pub(crate) pkce_verifier: Option<PkceCodeVerifier>,
155}
156
157/// Represents the [Client Credentials Flow](https://developer.spotify.com/documentation/web-api/tutorials/client-credentials-flow),
158/// as defined in [OAuth2 RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4).
159///
160/// Its use is recommended for apps usually running in the backend, that don't
161/// require accessing user information.
162///
163/// This flow does *not* require user authorisation, and thus does not permit
164/// making requests on the behalf of the user, so it can't access user data.
165#[derive(Clone, Copy, Debug)]
166pub struct ClientCredsFlow;
167
168/// Represents an unknown authentication flow, used when creating a
169/// [`Client`](crate::client::Client) via methods like
170/// [`from_refresh_token`](crate::client::Client::from_refresh_token).
171#[derive(Clone, Copy, Debug)]
172pub struct UnknownFlow;
173
174impl Token {
175    /// Create a new token, to be used with one of the [`from_access_token`](crate::client::Client::from_access_token) methods.
176    pub fn new(
177        access_token: impl Into<String>,
178        refresh_token: Option<&str>,
179        created_at: DateTime<Utc>,
180        expires_in: u64,
181        scopes: Option<Scopes>,
182    ) -> Self {
183        let access_token = AccessToken::new(access_token.into());
184        let refresh_token = refresh_token.map(|t| RefreshToken::new(t.to_owned()));
185        let expires_at =
186            created_at + chrono::Duration::seconds(i64::try_from(expires_in).unwrap_or(i64::MAX));
187
188        let scopes = scopes.map(|s| s.inner_vec());
189
190        Self {
191            access_token,
192            refresh_token,
193            expires_in,
194            created_at,
195            expires_at,
196            token_type: BasicTokenType::Bearer,
197            scopes,
198        }
199    }
200
201    /// Get the current access token secret.
202    pub fn secret(&self) -> &str {
203        self.access_token.secret()
204    }
205
206    /// Get the current refresh token. Some auth flows may not provide a refresh token,
207    /// in which case it will return `None`.
208    pub fn refresh_secret(&self) -> Option<&str> {
209        self.refresh_token.as_ref().map(|t| t.secret().as_str())
210    }
211
212    // Used to set the timestamp of a newly received token to the current time.
213    pub(crate) fn set_timestamps(self) -> Self {
214        let created_at = Utc::now();
215
216        // `self.expires_in` is a u64, so if converting from a u64 fails, use
217        // the max i64 value (unlikely to happen).
218        let expires_at = created_at
219            + chrono::Duration::seconds(i64::try_from(self.expires_in).unwrap_or(i64::MAX));
220
221        Self {
222            created_at,
223            expires_at,
224            ..self
225        }
226    }
227
228    /// Returns `true` if the access token has expired.
229    pub fn is_expired(&self) -> bool {
230        Utc::now() >= self.expires_at
231    }
232
233    /// Returns `true` if a refresh token is present.
234    pub fn is_refreshable(&self) -> bool {
235        self.refresh_token.is_some()
236    }
237}
238
239impl TokenResponse<BasicTokenType> for Token {
240    fn access_token(&self) -> &AccessToken {
241        &self.access_token
242    }
243
244    fn token_type(&self) -> &BasicTokenType {
245        &self.token_type
246    }
247
248    fn expires_in(&self) -> Option<Duration> {
249        Some(Duration::from_secs(self.expires_in))
250    }
251
252    fn refresh_token(&self) -> Option<&RefreshToken> {
253        self.refresh_token.as_ref()
254    }
255
256    fn scopes(&self) -> Option<&Vec<oauth2::Scope>> {
257        self.scopes.as_ref()
258    }
259}