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
use crate::{
alphabets, auth_urls,
clients::{BaseClient, OAuthClient},
generate_random_string,
http::{Form, HttpClient},
join_scopes, params,
sync::Mutex,
ClientResult, Config, Credentials, OAuth, Token,
};
use base64::{engine::general_purpose, Engine as _};
use std::collections::HashMap;
use std::sync::Arc;
use maybe_async::maybe_async;
use sha2::{Digest, Sha256};
use url::Url;
/// The [Authorization Code Flow with Proof Key for Code Exchange
/// (PKCE)][reference] client for the Spotify API.
///
/// This flow is very similar to the regular Authorization Code Flow, so please
/// read [`AuthCodeSpotify`](crate::AuthCodeSpotify) for more information about
/// it. The main difference in this case is that you can avoid storing your
/// client secret by generating a *code verifier* and a *code challenge*.
/// However, note that the refresh token obtained with PKCE will only work to
/// request the next one, after which it'll become invalid.
///
/// There's an [example][example-main] available to learn how to use this
/// client.
///
/// [reference]: https://developer.spotify.com/documentation/general/guides/authorization/code-flow
/// [example-main]: https://github.com/ramsayleung/rspotify/blob/master/examples/auth_code_pkce.rs
#[derive(Clone, Debug, Default)]
pub struct AuthCodePkceSpotify {
pub creds: Credentials,
pub oauth: OAuth,
pub config: Config,
pub token: Arc<Mutex<Option<Token>>>,
/// The code verifier for the authentication process
pub verifier: Option<String>,
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 AuthCodePkceSpotify {
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
}
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::GRANT_TYPE, params::GRANT_TYPE_REFRESH_TOKEN);
data.insert(params::REFRESH_TOKEN, refresh_token);
data.insert(params::CLIENT_ID, &self.creds.id);
let token = self.fetch_access_token(&data, None).await?;
if let Some(callback_fn) = &*self.get_config().token_callback_fn.clone() {
callback_fn.0(token.clone())?;
}
Ok(Some(token))
}
_ => Ok(None),
}
}
}
/// 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 AuthCodePkceSpotify {
fn get_oauth(&self) -> &OAuth {
&self.oauth
}
/// Note that the code verifier must be set at this point, either manually
/// or with [`Self::get_authorize_url`]. Otherwise, this function will
/// panic.
async fn request_token(&self, code: &str) -> ClientResult<()> {
log::info!("Requesting PKCE Auth Code token");
let verifier = self.verifier.as_ref().expect(
"Unknown code verifier. Try calling \
`AuthCodePkceSpotify::get_authorize_url` first or setting it \
yourself.",
);
let mut data = Form::new();
data.insert(params::CLIENT_ID, &self.creds.id);
data.insert(params::GRANT_TYPE, params::GRANT_TYPE_AUTH_CODE);
data.insert(params::CODE, code);
data.insert(params::REDIRECT_URI, &self.oauth.redirect_uri);
data.insert(params::CODE_VERIFIER, verifier);
let token = self.fetch_access_token(&data, None).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 AuthCodePkceSpotify {
/// Builds a new [`AuthCodePkceSpotify`] 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 [`AuthCodePkceSpotify`] 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()
}
}
/// Generate the verifier code and the challenge code.
fn generate_codes(verifier_bytes: usize) -> (String, String) {
log::info!("Generating PKCE codes");
debug_assert!(verifier_bytes >= 43);
debug_assert!(verifier_bytes <= 128);
// The code verifier is just the randomly generated string.
let verifier = generate_random_string(verifier_bytes, alphabets::PKCE_CODE_VERIFIER);
// The code challenge is the code verifier hashed with SHA256 and then
// encoded with base64url.
//
// NOTE: base64url != base64; it uses a different set of characters. See
// https://datatracker.ietf.org/doc/html/rfc4648#section-5 for more
// information.
let mut hasher = Sha256::new();
hasher.update(verifier.as_bytes());
let challenge = hasher.finalize();
let challenge = general_purpose::URL_SAFE_NO_PAD.encode(challenge);
(verifier, challenge)
}
/// Returns the URL needed to authorize the current client as the first step
/// in the authorization flow.
///
/// The parameter `verifier_bytes` is the length of the randomly generated
/// code verifier. Note that it must be between 43 and 128. If `None` is
/// given, a length of 43 will be used by default. See [the official
/// docs][reference] or [PKCE's RFC][rfce] for more information about the
/// code verifier.
///
/// [reference]: https://developer.spotify.com/documentation/general/guides/authorization/code-flow
/// [rfce]: https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
pub fn get_authorize_url(&mut self, verifier_bytes: Option<usize>) -> ClientResult<String> {
log::info!("Building auth URL");
let scopes = join_scopes(&self.oauth.scopes);
let verifier_bytes = verifier_bytes.unwrap_or(43);
let (verifier, challenge) = Self::generate_codes(verifier_bytes);
// The verifier will be needed later when requesting the token
self.verifier = Some(verifier);
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::CODE_CHALLENGE_METHOD,
params::CODE_CHALLENGE_METHOD_S256,
);
payload.insert(params::CODE_CHALLENGE, &challenge);
payload.insert(params::STATE, &self.oauth.state);
payload.insert(params::SCOPE, &scopes);
let request_url = self.auth_url(auth_urls::AUTHORIZE);
let parsed = Url::parse_with_params(&request_url, payload)?;
Ok(parsed.into())
}
}