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/// 
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}