Skip to main content

rspotify_s/
auth_code.rs

1use crate::{
2    auth_urls,
3    clients::{BaseClient, OAuthClient},
4    http::{Form, HttpClient},
5    join_scopes, params,
6    sync::Mutex,
7    ClientError, ClientResult, Config, Credentials, OAuth, Token,
8};
9
10use std::collections::HashMap;
11use std::sync::Arc;
12
13use maybe_async::maybe_async;
14use url::Url;
15
16/// The [Authorization Code Flow][reference] client for the Spotify API.
17///
18/// This includes user authorization, and thus has access to endpoints related
19/// to user private data, unlike the [Client Credentials
20/// Flow](crate::ClientCredsSpotify) client. See [`BaseClient`] and
21/// [`OAuthClient`] for the available endpoints.
22///
23/// If you're developing a CLI application, you might be interested in the `cli`
24/// feature. This brings the `prompt_for_token` method to automatically follow
25/// the flow steps via user interaction.
26///
27/// Otherwise, these are the steps to be followed to authenticate your app:
28///
29/// 0. Generate a request URL with [`Self::get_authorize_url`].
30/// 1. The user logs in with the request URL. They will be redirected to the
31///    given redirect URI, including a code in the URL parameters. This happens
32///    on your side.
33/// 2. The code obtained in the previous step is parsed with
34///    [`Self::parse_response_code`].
35/// 3. The code is sent to Spotify in order to obtain an access token with
36///    [`Self::request_token`].
37/// 4. Finally, this access token can be used internally for the requests.
38///    It may expire relatively soon, so it can be refreshed with the refresh
39///    token (obtained in the previous step as well) using
40///    [`Self::refresh_token`]. Otherwise, a new access token may be generated
41///    from scratch by repeating these steps, but the advantage of refreshing it
42///    is that this doesn't require the user to log in, and that it's a simpler
43///    procedure.
44///
45///    See [this related example][example-refresh-token] to learn more about
46///    refreshing tokens.
47///
48/// There's a [webapp example][example-webapp] for more details on how you can
49/// implement it for something like a web server, or [this one][example-main]
50/// for a CLI use case.
51///
52/// An example of the CLI authentication:
53///
54/// ![demo](https://raw.githubusercontent.com/ramsayleung/rspotify/master/doc/images/rspotify.gif)
55///
56/// Note: even if your script does not have an accessible URL, you will have to
57/// specify a redirect URI. It doesn't need to work, you can use
58/// `http://localhost:8888/callback` for example, which will also have the code
59/// appended like so: `http://localhost/?code=...`.
60///
61/// [reference]: https://developer.spotify.com/documentation/web-api/tutorials/code-flow
62/// [example-main]: https://github.com/ramsayleung/rspotify/blob/master/examples/auth_code.rs
63/// [example-webapp]: https://github.com/ramsayleung/rspotify/tree/master/examples/webapp
64/// [example-refresh-token]: https://github.com/ramsayleung/rspotify/blob/master/examples/with_refresh_token.rs
65#[derive(Clone, Debug, Default)]
66pub struct AuthCodeSpotify {
67    pub creds: Credentials,
68    pub oauth: OAuth,
69    pub config: Config,
70    pub token: Arc<Mutex<Option<Token>>>,
71    pub(crate) http: HttpClient,
72}
73
74/// This client has access to the base methods.
75#[cfg_attr(target_arch = "wasm32", maybe_async(?Send))]
76#[cfg_attr(not(target_arch = "wasm32"), maybe_async)]
77impl BaseClient for AuthCodeSpotify {
78    fn get_http(&self) -> &HttpClient {
79        &self.http
80    }
81
82    fn get_token(&self) -> Arc<Mutex<Option<Token>>> {
83        Arc::clone(&self.token)
84    }
85
86    fn get_creds(&self) -> &Credentials {
87        &self.creds
88    }
89
90    fn get_config(&self) -> &Config {
91        &self.config
92    }
93
94    /// Refetch the current access token given a refresh token. May return
95    /// `None` if there's no access/refresh token.
96    async fn refetch_token(&self) -> ClientResult<Option<Token>> {
97        match self.token.lock().await.unwrap().as_ref() {
98            Some(Token {
99                refresh_token: Some(refresh_token),
100                ..
101            }) => {
102                let mut data = Form::new();
103                data.insert(params::REFRESH_TOKEN, refresh_token);
104                data.insert(params::GRANT_TYPE, params::REFRESH_TOKEN);
105
106                let headers = self
107                    .creds
108                    .auth_headers()
109                    .expect("No client secret set in the credentials.");
110                let mut token = self.fetch_access_token(&data, Some(&headers)).await?;
111
112                token.refresh_token = Some(refresh_token.to_string());
113
114                if let Some(callback_fn) = &*self.get_config().token_callback_fn.clone() {
115                    callback_fn.0(token.clone())?;
116                }
117
118                Ok(Some(token))
119            }
120            _ => {
121                log::warn!("Can not refresh token! Token missing!");
122                Err(ClientError::InvalidToken)
123            }
124        }
125    }
126}
127
128/// This client includes user authorization, so it has access to the user
129/// private endpoints in [`OAuthClient`].
130#[cfg_attr(target_arch = "wasm32", maybe_async(?Send))]
131#[cfg_attr(not(target_arch = "wasm32"), maybe_async)]
132impl OAuthClient for AuthCodeSpotify {
133    fn get_oauth(&self) -> &OAuth {
134        &self.oauth
135    }
136
137    /// Obtains a user access token given a code, as part of the OAuth
138    /// authentication. The access token will be saved internally.
139    async fn request_token(&self, code: &str) -> ClientResult<()> {
140        log::info!("Requesting Auth Code token");
141
142        let scopes = join_scopes(&self.oauth.scopes);
143
144        let mut data = Form::new();
145        data.insert(params::GRANT_TYPE, params::GRANT_TYPE_AUTH_CODE);
146        data.insert(params::REDIRECT_URI, &self.oauth.redirect_uri);
147        data.insert(params::CODE, code);
148        data.insert(params::SCOPE, &scopes);
149        data.insert(params::STATE, &self.oauth.state);
150
151        let headers = self
152            .creds
153            .auth_headers()
154            .expect("No client secret set in the credentials.");
155
156        let token = self.fetch_access_token(&data, Some(&headers)).await?;
157
158        if let Some(callback_fn) = &*self.get_config().token_callback_fn.clone() {
159            callback_fn.0(token.clone())?;
160        }
161
162        *self.token.lock().await.unwrap() = Some(token);
163
164        self.write_token_cache().await
165    }
166}
167
168impl AuthCodeSpotify {
169    /// Builds a new [`AuthCodeSpotify`] given a pair of client credentials and
170    /// OAuth information.
171    #[must_use]
172    pub fn new(creds: Credentials, oauth: OAuth) -> Self {
173        Self {
174            creds,
175            oauth,
176            ..Default::default()
177        }
178    }
179
180    /// Build a new [`AuthCodeSpotify`] from an already generated token. Note
181    /// that once the token expires this will fail to make requests, as the
182    /// client credentials aren't known.
183    #[must_use]
184    pub fn from_token(token: Token) -> Self {
185        Self {
186            token: Arc::new(Mutex::new(Some(token))),
187            ..Default::default()
188        }
189    }
190
191    /// Same as [`Self::new`] but with an extra parameter to configure the
192    /// client.
193    #[must_use]
194    pub fn with_config(creds: Credentials, oauth: OAuth, config: Config) -> Self {
195        Self {
196            creds,
197            oauth,
198            config,
199            ..Default::default()
200        }
201    }
202
203    /// Build a new [`AuthCodeSpotify`] from an already generated token and
204    /// config. Use this to be able to refresh a token.
205    #[must_use]
206    pub fn from_token_with_config(
207        token: Token,
208        creds: Credentials,
209        oauth: OAuth,
210        config: Config,
211    ) -> Self {
212        Self {
213            token: Arc::new(Mutex::new(Some(token))),
214            creds,
215            oauth,
216            config,
217            ..Default::default()
218        }
219    }
220
221    /// Returns the URL needed to authorize the current client as the first step
222    /// in the authorization flow.
223    pub fn get_authorize_url(&self, show_dialog: bool) -> ClientResult<String> {
224        log::info!("Building auth URL");
225
226        let scopes = join_scopes(&self.oauth.scopes);
227
228        let mut payload: HashMap<&str, &str> = HashMap::new();
229        payload.insert(params::CLIENT_ID, &self.creds.id);
230        payload.insert(params::RESPONSE_TYPE, params::RESPONSE_TYPE_CODE);
231        payload.insert(params::REDIRECT_URI, &self.oauth.redirect_uri);
232        payload.insert(params::SCOPE, &scopes);
233        payload.insert(params::STATE, &self.oauth.state);
234
235        if show_dialog {
236            payload.insert(params::SHOW_DIALOG, "true");
237        }
238
239        let request_url = self.auth_url(auth_urls::AUTHORIZE);
240        let parsed = Url::parse_with_params(&request_url, payload)?;
241        Ok(parsed.into())
242    }
243}