From 81f13e2f9458a378d70d864716c2d915ad8cdaa4 Mon Sep 17 00:00:00 2001 From: Henrik Friedrichsen Date: Thu, 19 Sep 2024 18:02:14 +0200 Subject: [PATCH] Implement OAuth2 login flow The old user/password flow is deprecated and broken Fixes #1500 --- Cargo.lock | 1 + Cargo.toml | 1 + src/authentication.rs | 156 ++++++++++++------------------------------ src/config.rs | 1 - src/spotify.rs | 3 +- 5 files changed, 46 insertions(+), 116 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1f6d7d63..b22576cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2465,6 +2465,7 @@ dependencies = [ "ioctl-rs", "libc", "librespot-core", + "librespot-oauth", "librespot-playback", "librespot-protocol", "log", diff --git a/Cargo.toml b/Cargo.toml index 128cc243..6c68e8a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ futures = "0.3" ioctl-rs = {version = "0.2", optional = true} libc = "0.2.158" librespot-core = { git = "https://github.com/librespot-org/librespot", branch = "dev" } +librespot-oauth = { git = "https://github.com/librespot-org/librespot", branch = "dev" } librespot-playback = { git = "https://github.com/librespot-org/librespot", branch = "dev" } librespot-protocol = { git = "https://github.com/librespot-org/librespot", branch = "dev" } log = "0.4.22" diff --git a/src/authentication.rs b/src/authentication.rs index ccbdb6e7..f26b6de2 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -1,22 +1,42 @@ -use std::process::Command; - -use cursive::traits::Resizable; -use cursive::view::Nameable; -use cursive::views::*; -use cursive::Cursive; - use librespot_core::authentication::Credentials as RespotCredentials; use librespot_core::cache::Cache; -use librespot_protocol::authentication::AuthenticationType; +use librespot_oauth::get_access_token; use log::info; use crate::config::{self, Config}; use crate::spotify::Spotify; -use crate::ui::create_cursive; + +pub const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; +pub const CLIENT_REDIRECT_URI: &str = "http://127.0.0.1:8989/login"; + +static OAUTH_SCOPES: &[&str] = &[ + "playlist-modify", + "playlist-modify-private", + "playlist-modify-public", + "playlist-read", + "playlist-read-collaborative", + "playlist-read-private", + "streaming", + "user-follow-modify", + "user-follow-read", + "user-library-modify", + "user-library-read", + "user-modify", + "user-modify-playback-state", + "user-modify-private", + "user-personalized", + "user-read-currently-playing", + "user-read-email", + "user-read-play-history", + "user-read-playback-position", + "user-read-playback-state", + "user-read-private", + "user-read-recently-played", + "user-top-read", +]; /// Get credentials for use with librespot. This first tries to get cached credentials. If no cached -/// credentials are available, it will either try to get them from the user configured commands, or -/// if that fails, it will prompt the user on stdout. +/// credentials are available it will initiate the OAuth2 login process. pub fn get_credentials(configuration: &Config) -> Result { let mut credentials = { let cache = Cache::new(Some(config::cache_path("librespot")), None, None, None) @@ -28,19 +48,8 @@ pub fn get_credentials(configuration: &Config) -> Result { - info!("Attempting to resolve credentials via username/password commands"); - let creds = configuration - .values() - .credentials - .clone() - .unwrap_or_default(); - - match (creds.username_cmd, creds.password_cmd) { - (Some(username_cmd), Some(password_cmd)) => { - credentials_eval(&username_cmd, &password_cmd)? - } - _ => credentials_prompt(None)?, - } + info!("Attempting to login via OAuth2"); + credentials_prompt(None)? } } }; @@ -54,102 +63,21 @@ pub fn get_credentials(configuration: &Config) -> Result) -> Result { if let Some(message) = error_message { - let mut siv = create_cursive().unwrap(); - let dialog = cursive::views::Dialog::around(cursive::views::TextView::new(format!( - "Connection error:\n{message}" - ))) - .button("Ok", |s| s.quit()); - siv.add_layer(dialog); - siv.run(); + eprintln!("Connection error: {message}"); } create_credentials() } pub fn create_credentials() -> Result { - let mut login_cursive = create_cursive().unwrap(); - let info_buf = TextContent::new("Please login to Spotify\n"); - let info_view = Dialog::around(TextView::new_with_content(info_buf)) - .button("Login", move |s| { - let login_view = Dialog::new() - .title("Spotify login") - .content( - ListView::new() - .child( - "Username", - EditView::new().with_name("spotify_user").fixed_width(18), - ) - .child( - "Password", - EditView::new() - .secret() - .with_name("spotify_password") - .fixed_width(18), - ), - ) - .button("Login", |s| { - let username = s - .call_on_name("spotify_user", |view: &mut EditView| view.get_content()) - .unwrap() - .to_string(); - let auth_data = s - .call_on_name("spotify_password", |view: &mut EditView| view.get_content()) - .unwrap() - .to_string() - .as_bytes() - .to_vec(); - s.set_user_data::>(Ok(RespotCredentials { - username: Some(username), - auth_type: AuthenticationType::AUTHENTICATION_USER_PASS, - auth_data, - })); - s.quit(); - }) - .button("Quit", Cursive::quit); - s.pop_layer(); - s.add_layer(login_view); - }) - .button("Quit", Cursive::quit); - - login_cursive.add_layer(info_view); - login_cursive.run(); - - login_cursive - .user_data() - .cloned() - .unwrap_or_else(|| Err("Didn't obtain any credentials".to_string())) -} - -pub fn credentials_eval( - username_cmd: &str, - password_cmd: &str, -) -> Result { - fn eval(cmd: &str) -> Result, String> { - println!("Executing \"{}\"", cmd); - let mut result = Command::new("sh") - .args(["-c", cmd]) - .output() - .map_err(|e| e.to_string())? - .stdout; - if let Some(&last_byte) = result.last() { - if last_byte == 10 { - result.pop(); - } - } - - Ok(result) - } - - println!("Retrieving username"); - let username = String::from_utf8_lossy(&eval(username_cmd)?).into(); - println!("Retrieving password"); - let password = eval(password_cmd)?; - - Ok(RespotCredentials { - username: Some(username), - auth_type: AuthenticationType::AUTHENTICATION_USER_PASS, - auth_data: password, - }) + println!("To login you need to perform OAuth2 authorization using your web browser\n"); + get_access_token( + SPOTIFY_CLIENT_ID, + CLIENT_REDIRECT_URI, + OAUTH_SCOPES.to_vec(), + ) + .map(|token| RespotCredentials::with_access_token(token.access_token)) + .map_err(|e| e.to_string()) } #[derive(Serialize, Deserialize, Debug)] diff --git a/src/config.rs b/src/config.rs index 11a9ec2d..1a89fb7e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,7 +14,6 @@ use crate::model::playable::Playable; use crate::queue; use crate::serialization::{Serializer, CBOR, TOML}; -pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b"; pub const CACHE_VERSION: u16 = 1; pub const DEFAULT_COMMAND_KEY: char = ':'; diff --git a/src/spotify.rs b/src/spotify.rs index 40360fd5..3fa08f14 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -21,6 +21,7 @@ use tokio::sync::mpsc; use url::Url; use crate::application::ASYNC_RUNTIME; +use crate::authentication::SPOTIFY_CLIENT_ID; use crate::config; use crate::events::{Event, EventManager}; use crate::model::playable::Playable; @@ -129,7 +130,7 @@ impl Spotify { /// Generate the librespot [SessionConfig] used when creating a [Session]. pub fn session_config(cfg: &config::Config) -> SessionConfig { let mut session_config = librespot_core::SessionConfig { - client_id: config::CLIENT_ID.to_string(), + client_id: SPOTIFY_CLIENT_ID.to_string(), ..Default::default() }; match env::var("http_proxy") {