Skip to content

Commit

Permalink
auth: add option "k" to allow smooth login using Token instead of pas…
Browse files Browse the repository at this point in the history
…sword

fix: copy of github.com/librespot-org/issues/1245
  • Loading branch information
CarteKiwi committed Jan 24, 2024
1 parent f5a46c6 commit f694790
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ spotify_appkey.key
.history
*.save
*.*~
/.vs
8 changes: 8 additions & 0 deletions core/src/authentication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ impl Credentials {
}
}

pub fn with_token(username: impl Into<String>, token: impl Into<String>) -> Credentials {
Credentials {
username: username.into(),
auth_type: AuthenticationType::AUTHENTICATION_SPOTIFY_TOKEN,
auth_data: token.into().into_bytes(),
}
}

pub fn with_blob(
username: impl Into<String>,
encrypted_blob: impl AsRef<[u8]>,
Expand Down
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
13 changes: 13 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ fn get_setup() -> Setup {
#[cfg(feature = "passthrough-decoder")]
const PASSTHROUGH: &str = "passthrough";
const PASSWORD: &str = "password";
const TOKEN: &str = "token";
const PROXY: &str = "proxy";
const QUIET: &str = "quiet";
const SYSTEM_CACHE: &str = "system-cache";
Expand Down Expand Up @@ -268,6 +269,7 @@ fn get_setup() -> Setup {
#[cfg(feature = "passthrough-decoder")]
const PASSTHROUGH_SHORT: &str = "P";
const PASSWORD_SHORT: &str = "p";
const TOKEN_SHORT: &str = "k";
const EMIT_SINK_EVENTS_SHORT: &str = "Q";
const QUIET_SHORT: &str = "q";
const INITIAL_VOLUME_SHORT: &str = "R";
Expand Down Expand Up @@ -452,6 +454,12 @@ fn get_setup() -> Setup {
"Password used to sign in with.",
"PASSWORD",
)
.optopt(
TOKEN_SHORT,
TOKEN,
"token used to sign in with.",
"TOKEN",
)
.optopt(
ONEVENT_SHORT,
ONEVENT,
Expand Down Expand Up @@ -1088,6 +1096,11 @@ fn get_setup() -> Setup {
empty_string_error_msg(PASSWORD, PASSWORD_SHORT);
}
Some(Credentials::with_password(username, password))
} else if let Some(token) = opt_str(TOKEN) {
if token.is_empty() {
empty_string_error_msg(TOKEN, TOKEN_SHORT);
}
Some(Credentials::with_token(username, token))
} else {
match cached_creds {
Some(creds) if username == creds.username => Some(creds),
Expand Down

0 comments on commit f694790

Please sign in to comment.