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