Implement OAuth2 login flow
The old user/password flow is deprecated and broken Fixes #1500
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2465,6 +2465,7 @@ dependencies = [
|
|||||||
"ioctl-rs",
|
"ioctl-rs",
|
||||||
"libc",
|
"libc",
|
||||||
"librespot-core",
|
"librespot-core",
|
||||||
|
"librespot-oauth",
|
||||||
"librespot-playback",
|
"librespot-playback",
|
||||||
"librespot-protocol",
|
"librespot-protocol",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ futures = "0.3"
|
|||||||
ioctl-rs = {version = "0.2", optional = true}
|
ioctl-rs = {version = "0.2", optional = true}
|
||||||
libc = "0.2.158"
|
libc = "0.2.158"
|
||||||
librespot-core = { git = "https://github.com/librespot-org/librespot", branch = "dev" }
|
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-playback = { git = "https://github.com/librespot-org/librespot", branch = "dev" }
|
||||||
librespot-protocol = { git = "https://github.com/librespot-org/librespot", branch = "dev" }
|
librespot-protocol = { git = "https://github.com/librespot-org/librespot", branch = "dev" }
|
||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
|
|||||||
@@ -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::authentication::Credentials as RespotCredentials;
|
||||||
use librespot_core::cache::Cache;
|
use librespot_core::cache::Cache;
|
||||||
use librespot_protocol::authentication::AuthenticationType;
|
use librespot_oauth::get_access_token;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
use crate::config::{self, Config};
|
use crate::config::{self, Config};
|
||||||
use crate::spotify::Spotify;
|
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
|
/// 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
|
/// credentials are available it will initiate the OAuth2 login process.
|
||||||
/// if that fails, it will prompt the user on stdout.
|
|
||||||
pub fn get_credentials(configuration: &Config) -> Result<RespotCredentials, String> {
|
pub fn get_credentials(configuration: &Config) -> Result<RespotCredentials, String> {
|
||||||
let mut credentials = {
|
let mut credentials = {
|
||||||
let cache = Cache::new(Some(config::cache_path("librespot")), None, None, None)
|
let cache = Cache::new(Some(config::cache_path("librespot")), None, None, None)
|
||||||
@@ -28,19 +48,8 @@ pub fn get_credentials(configuration: &Config) -> Result<RespotCredentials, Stri
|
|||||||
c
|
c
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
info!("Attempting to resolve credentials via username/password commands");
|
info!("Attempting to login via OAuth2");
|
||||||
let creds = configuration
|
credentials_prompt(None)?
|
||||||
.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)?,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -54,102 +63,21 @@ pub fn get_credentials(configuration: &Config) -> Result<RespotCredentials, Stri
|
|||||||
|
|
||||||
fn credentials_prompt(error_message: Option<String>) -> Result<RespotCredentials, String> {
|
fn credentials_prompt(error_message: Option<String>) -> Result<RespotCredentials, String> {
|
||||||
if let Some(message) = error_message {
|
if let Some(message) = error_message {
|
||||||
let mut siv = create_cursive().unwrap();
|
eprintln!("Connection error: {message}");
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
create_credentials()
|
create_credentials()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_credentials() -> Result<RespotCredentials, String> {
|
pub fn create_credentials() -> Result<RespotCredentials, String> {
|
||||||
let mut login_cursive = create_cursive().unwrap();
|
println!("To login you need to perform OAuth2 authorization using your web browser\n");
|
||||||
let info_buf = TextContent::new("Please login to Spotify\n");
|
get_access_token(
|
||||||
let info_view = Dialog::around(TextView::new_with_content(info_buf))
|
SPOTIFY_CLIENT_ID,
|
||||||
.button("Login", move |s| {
|
CLIENT_REDIRECT_URI,
|
||||||
let login_view = Dialog::new()
|
OAUTH_SCOPES.to_vec(),
|
||||||
.title("Spotify login")
|
)
|
||||||
.content(
|
.map(|token| RespotCredentials::with_access_token(token.access_token))
|
||||||
ListView::new()
|
.map_err(|e| e.to_string())
|
||||||
.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::<Result<RespotCredentials, String>>(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<RespotCredentials, String> {
|
|
||||||
fn eval(cmd: &str) -> Result<Vec<u8>, 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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ use crate::model::playable::Playable;
|
|||||||
use crate::queue;
|
use crate::queue;
|
||||||
use crate::serialization::{Serializer, CBOR, TOML};
|
use crate::serialization::{Serializer, CBOR, TOML};
|
||||||
|
|
||||||
pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b";
|
|
||||||
pub const CACHE_VERSION: u16 = 1;
|
pub const CACHE_VERSION: u16 = 1;
|
||||||
pub const DEFAULT_COMMAND_KEY: char = ':';
|
pub const DEFAULT_COMMAND_KEY: char = ':';
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use tokio::sync::mpsc;
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::application::ASYNC_RUNTIME;
|
use crate::application::ASYNC_RUNTIME;
|
||||||
|
use crate::authentication::SPOTIFY_CLIENT_ID;
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::events::{Event, EventManager};
|
use crate::events::{Event, EventManager};
|
||||||
use crate::model::playable::Playable;
|
use crate::model::playable::Playable;
|
||||||
@@ -129,7 +130,7 @@ impl Spotify {
|
|||||||
/// Generate the librespot [SessionConfig] used when creating a [Session].
|
/// Generate the librespot [SessionConfig] used when creating a [Session].
|
||||||
pub fn session_config(cfg: &config::Config) -> SessionConfig {
|
pub fn session_config(cfg: &config::Config) -> SessionConfig {
|
||||||
let mut session_config = librespot_core::SessionConfig {
|
let mut session_config = librespot_core::SessionConfig {
|
||||||
client_id: config::CLIENT_ID.to_string(),
|
client_id: SPOTIFY_CLIENT_ID.to_string(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
match env::var("http_proxy") {
|
match env::var("http_proxy") {
|
||||||
|
|||||||
Reference in New Issue
Block a user