Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Obtain spclient access token using login5 instead of keymaster (Fixes #1179 and #1245) #1246

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions core/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ struct SessionData {
client_brand_name: String,
client_model_name: String,
connection_id: String,
auth_blob: Vec<u8>,
time_delta: i64,
invalid: bool,
user_data: UserData,
Expand Down Expand Up @@ -174,6 +175,7 @@ impl Session {

info!("Authenticated as \"{}\" !", reusable_credentials.username);
self.set_username(&reusable_credentials.username);
self.set_auth_blob(&reusable_credentials.auth_data);
if let Some(cache) = self.cache() {
if store_credentials {
let cred_changed = cache
Expand Down Expand Up @@ -471,6 +473,14 @@ impl Session {
self.0.data.write().user_data.canonical_username = username.to_owned();
}

pub fn auth_blob(&self) -> Vec<u8> {
self.0.data.read().auth_blob.clone()
}

pub fn set_auth_blob(&self, auth_blob: &[u8]) {
self.0.data.write().auth_blob = auth_blob.to_owned();
}

pub fn country(&self) -> String {
self.0.data.read().user_data.country.clone()
}
Expand Down
98 changes: 92 additions & 6 deletions core/src/spclient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ use crate::{
},
connect::PutStateRequest,
extended_metadata::BatchedEntityRequest,
login5::{LoginRequest, LoginResponse},
},
token::Token,
version::spotify_version,
Expand All @@ -44,6 +45,7 @@ component! {
accesspoint: Option<SocketAddress> = None,
strategy: RequestStrategy = RequestStrategy::default(),
client_token: Option<Token> = None,
auth_token: Option<Token> = None,
}
}

Expand Down Expand Up @@ -149,6 +151,91 @@ impl SpClient {
Ok(())
}

async fn auth_token_request<M: Message>(&self, message: &M) -> Result<Bytes, Error> {
let client_token = self.client_token().await?;
let body = message.write_to_bytes()?;

let request = Request::builder()
.method(&Method::POST)
.uri("https://login5.spotify.com/v3/login")
.header(ACCEPT, HeaderValue::from_static("application/x-protobuf"))
.header(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?)
.body(Body::from(body))?;

self.session().http_client().request_body(request).await
}

pub async fn auth_token(&self) -> Result<Token, Error> {
let auth_token = self.lock(|inner| {
if let Some(token) = &inner.auth_token {
if token.is_expired() {
inner.auth_token = None;
}
}
inner.auth_token.clone()
});

if let Some(auth_token) = auth_token {
return Ok(auth_token);
}

let client_id = match OS {
"macos" | "windows" => self.session().client_id(),
_ => SessionConfig::default().client_id,
};

let mut login_request = LoginRequest::new();
login_request.client_info.mut_or_insert_default().client_id = client_id;
login_request.client_info.mut_or_insert_default().device_id =
self.session().device_id().to_string();

let stored_credential = login_request.mut_stored_credential();
stored_credential.username = self.session().username().to_string();
stored_credential.data = self.session().auth_blob().clone();

let mut response = self.auth_token_request(&login_request).await?;
let mut count = 0;
const MAX_TRIES: u8 = 3;

let token_response = loop {
count += 1;

let message = LoginResponse::parse_from_bytes(&response)?;
// TODO: Handle hash cash stuff
if message.has_ok() {
break message.ok().to_owned();
}

if count < MAX_TRIES {
response = self.auth_token_request(&login_request).await?;
} else {
return Err(Error::failed_precondition(format!(
"Unable to solve any of {MAX_TRIES} hash cash challenges"
)));
}
};

let auth_token = Token {
access_token: token_response.access_token.clone(),
expires_in: Duration::from_secs(
token_response
.access_token_expires_in
.try_into()
.unwrap_or(3600),
),
token_type: "Bearer".to_string(),
scopes: vec![],
timestamp: Instant::now(),
};
self.lock(|inner| {
inner.auth_token = Some(auth_token.clone());
});

trace!("Got auth token: {:?}", auth_token);

Ok(auth_token)
}

async fn client_token_request<M: Message>(&self, message: &M) -> Result<Bytes, Error> {
let body = message.write_to_bytes()?;

Expand Down Expand Up @@ -469,19 +556,18 @@ impl SpClient {
.body(Body::from(body.to_owned()))?;

// Reconnection logic: keep getting (cached) tokens because they might have expired.
let token = self
.session()
.token_provider()
.get_token("playlist-read")
.await?;
let auth_token = self.auth_token().await?;

let headers_mut = request.headers_mut();
if let Some(ref hdrs) = headers {
*headers_mut = hdrs.clone();
}
headers_mut.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("{} {}", token.token_type, token.access_token,))?,
HeaderValue::from_str(&format!(
"{} {}",
auth_token.token_type, auth_token.access_token,
))?,
);

match self.client_token().await {
Expand Down
7 changes: 7 additions & 0 deletions protocol/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ fn compile() {
proto_dir.join("playlist_permission.proto"),
proto_dir.join("playlist4_external.proto"),
proto_dir.join("spotify/clienttoken/v0/clienttoken_http.proto"),
proto_dir.join("spotify/login5/v3/challenges/code.proto"),
proto_dir.join("spotify/login5/v3/challenges/hashcash.proto"),
proto_dir.join("spotify/login5/v3/client_info.proto"),
proto_dir.join("spotify/login5/v3/credentials/credentials.proto"),
proto_dir.join("spotify/login5/v3/identifiers/identifiers.proto"),
proto_dir.join("spotify/login5/v3/login5.proto"),
proto_dir.join("spotify/login5/v3/user_info.proto"),
proto_dir.join("storage-resolve.proto"),
proto_dir.join("user_attributes.proto"),
// TODO: remove these legacy protobufs when we are on the new API completely
Expand Down