rspotify_model/
auth.rs

1//! All objects related to the auth flows defined by Spotify API
2
3use crate::{
4    custom_serde::{duration_second, space_separated_scopes},
5    ModelResult,
6};
7
8use std::{
9    collections::{HashMap, HashSet},
10    fs,
11    io::{Read, Write},
12    path::Path,
13};
14
15use chrono::{DateTime, Duration, TimeDelta, Utc};
16use serde::{Deserialize, Serialize};
17
18/// Spotify access token information
19///
20/// [Reference](https://developer.spotify.com/documentation/general/guides/authorization/)
21#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
22pub struct Token {
23    /// An access token that can be provided in subsequent calls
24    pub access_token: String,
25    /// The time period for which the access token is valid.
26    #[serde(with = "duration_second")]
27    pub expires_in: Duration,
28    /// The valid time for which the access token is available represented
29    /// in ISO 8601 combined date and time.
30    pub expires_at: Option<DateTime<Utc>>,
31    /// A token that can be sent to the Spotify Accounts service
32    /// in place of an authorization code
33    pub refresh_token: Option<String>,
34    /// A list of [scopes](https://developer.spotify.com/documentation/general/guides/authorization/scopes/)
35    /// which have been granted for this `access_token`
36    ///
37    /// You may use the `scopes!` macro in
38    /// [`rspotify-macros`](https://docs.rs/rspotify-macros) to build it at
39    /// compile time easily.
40    // The token response from spotify is singular, hence the rename to `scope`
41    #[serde(default, with = "space_separated_scopes", rename = "scope")]
42    pub scopes: HashSet<String>,
43}
44
45impl Default for Token {
46    fn default() -> Self {
47        Self {
48            access_token: String::new(),
49            expires_in: Duration::try_seconds(0).unwrap(),
50            expires_at: Some(Utc::now()),
51            refresh_token: None,
52            scopes: HashSet::new(),
53        }
54    }
55}
56
57impl Token {
58    /// Tries to initialize the token from a cache file.
59    pub fn from_cache<T: AsRef<Path>>(path: T) -> ModelResult<Self> {
60        let mut file = fs::File::open(path)?;
61        let mut tok_str = String::new();
62        file.read_to_string(&mut tok_str)?;
63        let tok = serde_json::from_str(&tok_str)?;
64
65        Ok(tok)
66    }
67
68    /// Saves the token information into its cache file.
69    pub fn write_cache<T: AsRef<Path>>(&self, path: T) -> ModelResult<()> {
70        let token_info = serde_json::to_string(&self)?;
71
72        let mut file = fs::OpenOptions::new()
73            .write(true)
74            .create(true)
75            .truncate(true)
76            .open(path)?;
77        file.set_len(0)?;
78        file.write_all(token_info.as_bytes())?;
79
80        Ok(())
81    }
82
83    /// Check if the token is expired. It includes a margin of 10 seconds (which
84    /// is how much a request would take in the worst case scenario).
85    #[must_use]
86    pub fn is_expired(&self) -> bool {
87        self.expires_at
88            .is_none_or(|expiration| Utc::now() + TimeDelta::try_seconds(10).unwrap() >= expiration)
89    }
90
91    /// Generates an HTTP token authorization header with proper formatting
92    #[must_use]
93    pub fn auth_headers(&self) -> HashMap<String, String> {
94        let auth = "authorization".to_owned();
95        let value = format!("Bearer {}", self.access_token);
96
97        let mut headers = HashMap::new();
98        headers.insert(auth, value);
99        headers
100    }
101}
102
103#[cfg(test)]
104mod test {
105    use std::collections::HashSet;
106
107    use crate::Token;
108    use serde_json::json;
109
110    #[test]
111    fn test_bearer_auth() {
112        let tok = Token {
113            access_token: "access_token".to_string(),
114            ..Default::default()
115        };
116
117        let headers = tok.auth_headers();
118        assert_eq!(headers.len(), 1);
119        assert_eq!(
120            headers.get("authorization"),
121            Some(&"Bearer access_token".to_owned())
122        );
123    }
124
125    #[test]
126    fn test_token_deserialize() {
127        let mut scopes = HashSet::<String>::new();
128        scopes.insert("user-read-email".to_owned());
129        let tok = Token {
130            access_token: "access_token".to_string(),
131            scopes,
132            ..Default::default()
133        };
134        let value = json!(tok);
135        let token = serde_json::from_value::<Token>(value);
136        assert!(token.is_ok());
137        assert_eq!(token.unwrap().scopes, tok.scopes);
138    }
139}