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

Credentials with access token (oauth) #1309

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ https://github.com/librespot-org/librespot
- [core] Cache resolved access points during runtime (breaking)
- [core] `FileId` is moved out of `SpotifyId`. For now it will be re-exported.
- [core] Report actual platform data on login
- [core] Support `Session` authentication with a Spotify access token
- [core] `Credentials.username` is now an `Option` (breaking)
- [main] `autoplay {on|off}` now acts as an override. If unspecified, `librespot`
now follows the setting in the Connect client that controls it. (breaking)
- [metadata] Most metadata is now retrieved with the `spclient` (breaking)
Expand Down Expand Up @@ -95,6 +97,7 @@ https://github.com/librespot-org/librespot
- [main] Add an event worker thread that runs async to the main thread(s) but
sync to itself to prevent potential data races for event consumers
- [metadata] All metadata fields in the protobufs are now exposed (breaking)
- [oauth] Standalone module to obtain Spotify access token using OAuth authorization code flow.
- [playback] Explicit tracks are skipped if the controlling Connect client has
disabled such content. Applications that use librespot as a library without
Connect should use the 'filter-explicit-content' user attribute in the session.
Expand Down
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ version = "0.5.0-dev"
path = "protocol"
version = "0.5.0-dev"

[dependencies.librespot-oauth]
kingosticks marked this conversation as resolved.
Show resolved Hide resolved
path = "oauth"
version = "0.5.0-dev"

[dependencies]
data-encoding = "2.5"
env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] }
Expand Down
4 changes: 4 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ license = "MIT"
repository = "https://github.com/librespot-org/librespot"
edition = "2021"

[dependencies.librespot-oauth]
path = "../oauth"
version = "0.5.0-dev"

[dependencies.librespot-protocol]
path = "../protocol"
version = "0.5.0-dev"
Expand Down
22 changes: 15 additions & 7 deletions core/src/authentication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ impl From<AuthenticationError> for Error {
/// The credentials are used to log into the Spotify API.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct Credentials {
pub username: String,
pub username: Option<String>,

#[serde(serialize_with = "serialize_protobuf_enum")]
#[serde(deserialize_with = "deserialize_protobuf_enum")]
Expand All @@ -50,19 +50,27 @@ impl Credentials {
///
/// let creds = Credentials::with_password("my account", "my password");
/// ```
pub fn with_password(username: impl Into<String>, password: impl Into<String>) -> Credentials {
Credentials {
username: username.into(),
pub fn with_password(username: impl Into<String>, password: impl Into<String>) -> Self {
Self {
username: Some(username.into()),
auth_type: AuthenticationType::AUTHENTICATION_USER_PASS,
auth_data: password.into().into_bytes(),
}
}

pub fn with_access_token(token: impl Into<String>) -> Self {
Self {
username: None,
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]>,
device_id: impl AsRef<[u8]>,
) -> Result<Credentials, Error> {
) -> Result<Self, Error> {
fn read_u8<R: Read>(stream: &mut R) -> io::Result<u8> {
let mut data = [0u8];
stream.read_exact(&mut data)?;
Expand Down Expand Up @@ -136,8 +144,8 @@ impl Credentials {
read_u8(&mut cursor)?;
let auth_data = read_bytes(&mut cursor)?;

Ok(Credentials {
username,
Ok(Self {
username: Some(username),
auth_type,
auth_data,
})
Expand Down
13 changes: 8 additions & 5 deletions core/src/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,12 @@ pub async fn authenticate(
};

let mut packet = ClientResponseEncrypted::new();
packet
.login_credentials
.mut_or_insert_default()
.set_username(credentials.username);
if let Some(username) = credentials.username {
packet
.login_credentials
.mut_or_insert_default()
.set_username(username);
}
packet
.login_credentials
.mut_or_insert_default()
Expand Down Expand Up @@ -133,6 +135,7 @@ pub async fn authenticate(
let cmd = PacketType::Login;
let data = packet.write_to_bytes()?;

debug!("Authenticating with AP using {:?}", credentials.auth_type);
transport.send((cmd as u8, data)).await?;
let (cmd, data) = transport
.next()
Expand All @@ -144,7 +147,7 @@ pub async fn authenticate(
let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?;

let reusable_credentials = Credentials {
username: welcome_data.canonical_username().to_owned(),
username: Some(welcome_data.canonical_username().to_owned()),
auth_type: welcome_data.reusable_auth_credentials_type(),
auth_data: welcome_data.reusable_auth_credentials().to_owned(),
};
Expand Down
21 changes: 21 additions & 0 deletions core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use tokio::sync::{
};
use url::ParseError;

use librespot_oauth::OAuthError;

#[cfg(feature = "with-dns-sd")]
use dns_sd::DNSError;

Expand Down Expand Up @@ -287,6 +289,25 @@ impl fmt::Display for Error {
}
}

impl From<OAuthError> for Error {
fn from(err: OAuthError) -> Self {
use OAuthError::*;
match err {
AuthCodeBadUri { .. }
| AuthCodeNotFound { .. }
| AuthCodeListenerRead
| AuthCodeListenerParse => Error::unavailable(err),
AuthCodeStdinRead
| AuthCodeListenerBind { .. }
| AuthCodeListenerTerminated
| AuthCodeListenerWrite
| Recv
| ExchangeCode { .. } => Error::internal(err),
_ => Error::failed_precondition(err),
}
}
}

impl From<DecodeError> for Error {
fn from(err: DecodeError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
Expand Down
76 changes: 61 additions & 15 deletions core/src/session.rs
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes;
use futures_core::TryStream;
use futures_util::{future, ready, StreamExt, TryStreamExt};
use librespot_protocol::authentication::AuthenticationType;
use num_traits::FromPrimitive;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
Expand All @@ -22,13 +23,13 @@ use tokio::{sync::mpsc, time::Instant};
use tokio_stream::wrappers::UnboundedReceiverStream;

use crate::{
apresolve::ApResolver,
apresolve::{ApResolver, SocketAddress},
audio_key::AudioKeyManager,
authentication::Credentials,
cache::Cache,
channel::ChannelManager,
config::SessionConfig,
connection::{self, AuthenticationError},
connection::{self, AuthenticationError, Transport},
http_client::HttpClient,
mercury::MercuryManager,
packet::PacketType,
Expand Down Expand Up @@ -77,6 +78,7 @@ struct SessionData {
client_brand_name: String,
client_model_name: String,
connection_id: String,
auth_data: Vec<u8>,
roderickvd marked this conversation as resolved.
Show resolved Hide resolved
time_delta: i64,
invalid: bool,
user_data: UserData,
Expand Down Expand Up @@ -140,6 +142,46 @@ impl Session {
}))
}

async fn connect_inner(
&self,
access_point: SocketAddress,
credentials: Credentials,
) -> Result<(Credentials, Transport), Error> {
let mut transport = connection::connect(
&access_point.0,
access_point.1,
self.config().proxy.as_ref(),
)
.await?;
let mut reusable_credentials = connection::authenticate(
&mut transport,
credentials.clone(),
&self.config().device_id,
)
.await?;

// Might be able to remove this once keymaster is replaced with login5.
if credentials.auth_type == AuthenticationType::AUTHENTICATION_SPOTIFY_TOKEN {
trace!(
"Reconnect using stored credentials as token authed sessions cannot use keymaster."
);
transport = connection::connect(
&access_point.0,
access_point.1,
self.config().proxy.as_ref(),
)
.await?;
reusable_credentials = connection::authenticate(
&mut transport,
reusable_credentials.clone(),
&self.config().device_id,
)
.await?;
}

Ok((reusable_credentials, transport))
}

pub async fn connect(
&self,
credentials: Credentials,
Expand All @@ -148,17 +190,8 @@ impl Session {
let (reusable_credentials, transport) = loop {
let ap = self.apresolver().resolve("accesspoint").await?;
info!("Connecting to AP \"{}:{}\"", ap.0, ap.1);
let mut transport =
connection::connect(&ap.0, ap.1, self.config().proxy.as_ref()).await?;

match connection::authenticate(
&mut transport,
credentials.clone(),
&self.config().device_id,
)
.await
{
Ok(creds) => break (creds, transport),
match self.connect_inner(ap, credentials.clone()).await {
Ok(ct) => break ct,
Err(e) => {
if let Some(AuthenticationError::LoginFailed(ErrorCode::TryAnotherAP)) =
e.error.downcast_ref::<AuthenticationError>()
Expand All @@ -172,8 +205,13 @@ impl Session {
}
};

info!("Authenticated as \"{}\" !", reusable_credentials.username);
self.set_username(&reusable_credentials.username);
let username = reusable_credentials
.username
.as_ref()
.map_or("UNKNOWN", |s| s.as_str());
info!("Authenticated as '{username}' !");
self.set_username(username);
self.set_auth_data(&reusable_credentials.auth_data);
if let Some(cache) = self.cache() {
if store_credentials {
let cred_changed = cache
Expand Down Expand Up @@ -471,6 +509,14 @@ impl Session {
username.clone_into(&mut self.0.data.write().user_data.canonical_username);
}

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

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

pub fn country(&self) -> String {
self.0.data.read().user_data.country.clone()
}
Expand Down
37 changes: 24 additions & 13 deletions examples/get_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,34 @@ const SCOPES: &str =

#[tokio::main]
async fn main() {
let session_config = SessionConfig::default();
let mut builder = env_logger::Builder::new();
builder.parse_filters("librespot=trace");
builder.init();

let mut session_config = SessionConfig::default();

let args: Vec<_> = env::args().collect();
if args.len() != 3 {
eprintln!("Usage: {} USERNAME PASSWORD", args[0]);
if args.len() == 3 {
// Only special client IDs have sufficient privileges e.g. Spotify's.
session_config.client_id = args[2].clone()
} else if args.len() != 2 {
eprintln!("Usage: {} ACCESS_TOKEN [CLIENT_ID]", args[0]);
return;
}
let access_token = &args[1];

println!("Connecting...");
let credentials = Credentials::with_password(&args[1], &args[2]);
let session = Session::new(session_config, None);

// Now create a new session with that token.
let session = Session::new(session_config.clone(), None);
let credentials = Credentials::with_access_token(access_token);
println!("Connecting with token..");
match session.connect(credentials, false).await {
Ok(()) => println!(
"Token: {:#?}",
session.token_provider().get_token(SCOPES).await.unwrap()
),
Err(e) => println!("Error connecting: {}", e),
}
Ok(()) => println!("Session username: {:#?}", session.username()),
Err(e) => {
println!("Error connecting: {e}");
return;
}
};

let token = session.token_provider().get_token(SCOPES).await.unwrap();
println!("Got me a token: {token:#?}");
}
8 changes: 4 additions & 4 deletions examples/play.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ async fn main() {
let audio_format = AudioFormat::default();

let args: Vec<_> = env::args().collect();
if args.len() != 4 {
eprintln!("Usage: {} USERNAME PASSWORD TRACK", args[0]);
if args.len() != 3 {
eprintln!("Usage: {} ACCESS_TOKEN TRACK", args[0]);
return;
}
let credentials = Credentials::with_password(&args[1], &args[2]);
let credentials = Credentials::with_access_token(&args[1]);

let mut track = SpotifyId::from_base62(&args[3]).unwrap();
let mut track = SpotifyId::from_base62(&args[2]).unwrap();
track.item_type = SpotifyItemType::Track;

let backend = audio_backend::find(None).unwrap();
Expand Down
8 changes: 4 additions & 4 deletions examples/play_connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@ async fn main() {
let connect_config = ConnectConfig::default();

let mut args: Vec<_> = env::args().collect();
let context_uri = if args.len() == 4 {
let context_uri = if args.len() == 3 {
args.pop().unwrap()
} else if args.len() == 3 {
} else if args.len() == 2 {
String::from("spotify:album:79dL7FLiJFOO0EoehUHQBv")
} else {
eprintln!("Usage: {} USERNAME PASSWORD (ALBUM URI)", args[0]);
eprintln!("Usage: {} ACCESS_TOKEN (ALBUM URI)", args[0]);
return;
};

let credentials = Credentials::with_password(&args[1], &args[2]);
let credentials = Credentials::with_access_token(&args[1]);
let backend = audio_backend::find(None).unwrap();

println!("Connecting...");
Expand Down
8 changes: 4 additions & 4 deletions examples/playlist_tracks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ async fn main() {
let session_config = SessionConfig::default();

let args: Vec<_> = env::args().collect();
if args.len() != 4 {
eprintln!("Usage: {} USERNAME PASSWORD PLAYLIST", args[0]);
if args.len() != 3 {
eprintln!("Usage: {} ACCESS_TOKEN PLAYLIST", args[0]);
return;
}
let credentials = Credentials::with_password(&args[1], &args[2]);
let credentials = Credentials::with_access_token(&args[1]);

let plist_uri = SpotifyId::from_uri(&args[3]).unwrap_or_else(|_| {
let plist_uri = SpotifyId::from_uri(&args[2]).unwrap_or_else(|_| {
eprintln!(
"PLAYLIST should be a playlist URI such as: \
\"spotify:playlist:37i9dQZF1DXec50AjHrNTq\""
Expand Down
Loading