Skip to content

Commit

Permalink
Add new auth method, but URL needs to be in window
Browse files Browse the repository at this point in the history
  • Loading branch information
jacksongoode committed Aug 24, 2024
1 parent 11bef15 commit cfd1f83
Show file tree
Hide file tree
Showing 9 changed files with 631 additions and 89 deletions.
436 changes: 430 additions & 6 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions psst-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ socks = { version = "0.3.4" }
tempfile = { version = "3.10.1" }
ureq = { version = "2.9.7", features = ["json"] }
url = { version = "2.5.2" }
oauth2 = { version = "4.4.2" }

# Cryptography
aes = { version = "0.8.4" }
Expand Down
28 changes: 14 additions & 14 deletions psst-core/src/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,27 @@ const AP_FALLBACK: &str = "ap.spotify.com:443";
#[serde(from = "SerializedCredentials")]
#[serde(into = "SerializedCredentials")]
pub struct Credentials {
pub username: String,
pub username: Option<String>,
pub auth_data: Vec<u8>,
pub auth_type: AuthenticationType,
}

impl Credentials {
pub fn from_username_and_password(username: String, password: String) -> Self {
Self {
username,
username: Some(username),
auth_type: AuthenticationType::AUTHENTICATION_USER_PASS,
auth_data: password.into_bytes(),
}
}

pub fn from_access_token(token: String) -> Self {
Self {
username: None,
auth_type: AuthenticationType::AUTHENTICATION_SPOTIFY_TOKEN,
auth_data: token.into_bytes(),
}
}
}

#[derive(Serialize, Deserialize)]
Expand All @@ -66,7 +74,7 @@ struct SerializedCredentials {
impl From<SerializedCredentials> for Credentials {
fn from(value: SerializedCredentials) -> Self {
Self {
username: value.username,
username: Some(value.username),
auth_data: value.auth_data.into_bytes(),
auth_type: value.auth_type.into(),
}
Expand All @@ -76,7 +84,7 @@ impl From<SerializedCredentials> for Credentials {
impl From<Credentials> for SerializedCredentials {
fn from(value: Credentials) -> Self {
Self {
username: value.username,
username: value.username.unwrap_or_default(),
auth_data: String::from_utf8(value.auth_data)
.expect("Invalid UTF-8 in serialized credentials"),
auth_type: value.auth_type as _,
Expand Down Expand Up @@ -231,14 +239,6 @@ impl Transport {
pub fn authenticate(&mut self, credentials: Credentials) -> Result<Credentials, Error> {
use crate::protocol::{authentication::APWelcome, keyexchange::APLoginFailed};

// Having an empty username or auth_data causes an unclear error message, so replace it with invalid credentials.
if credentials.username.is_empty() || credentials.auth_data.is_empty() {
return Err(Error::AuthFailed {
// code 12 = bad credentials
code: 12,
});
}

// Send a login request with the client credentials.
let request = client_response_encrypted(credentials);
self.encoder.encode(request)?;
Expand All @@ -251,7 +251,7 @@ impl Transport {
let welcome_data: APWelcome =
deserialize_protobuf(&response.payload).expect("Missing data");
Ok(Credentials {
username: welcome_data.canonical_username,
username: Some(welcome_data.canonical_username),
auth_data: welcome_data.reusable_auth_credentials,
auth_type: welcome_data.reusable_auth_credentials_type,
})
Expand Down Expand Up @@ -362,7 +362,7 @@ fn client_response_encrypted(credentials: Credentials) -> ShannonMsg {

let response = ClientResponseEncrypted {
login_credentials: LoginCredentials {
username: Some(credentials.username),
username: credentials.username,
auth_data: Some(credentials.auth_data),
typ: credentials.auth_type,
},
Expand Down
1 change: 1 addition & 0 deletions psst-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ pub mod metadata;
pub mod player;
pub mod session;
pub mod util;
pub mod oauth;

pub use psst_protocol as protocol;
142 changes: 142 additions & 0 deletions psst-core/src/oauth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
use log::{debug, error, info, trace};
use oauth2::reqwest::http_client;
use oauth2::{
basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge,
RedirectUrl, Scope, TokenResponse, TokenUrl,
};
use std::io;
use std::{
io::{BufRead, BufReader, Write},
net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener},
process::exit,
sync::mpsc,
};
use url::Url;

fn get_code(redirect_url: &str) -> AuthorizationCode {
Url::parse(redirect_url)
.unwrap()
.query_pairs()
.find(|(key, _)| key == "code")
.map(|(_, code)| AuthorizationCode::new(code.into_owned()))
.expect("No code found in redirect URL")
}

fn get_authcode_stdin() -> AuthorizationCode {
println!("Provide redirect URL");
let mut buffer = String::new();
let stdin = io::stdin();
stdin.read_line(&mut buffer).unwrap();

get_code(buffer.trim())
}

fn get_authcode_listener(socket_address: SocketAddr) -> AuthorizationCode {
let listener = TcpListener::bind(socket_address).unwrap();

info!("OAuth server listening on {:?}", socket_address);

let Some(mut stream) = listener.incoming().flatten().next() else {
panic!("listener terminated without accepting a connection");
};

let mut reader = BufReader::new(&stream);

let mut request_line = String::new();
reader.read_line(&mut request_line).unwrap();

let redirect_url = request_line.split_whitespace().nth(1).unwrap();
let code = get_code(&("http://localhost".to_string() + redirect_url));

let message = "Authenticated! You can return to Psst.";
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
message.len(),
message
);
stream.write_all(response.as_bytes()).unwrap();

code
}

pub fn get_access_token(client_id: &str, redirect_port: u16) -> String {
let redirect_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), redirect_port);
let redirect_uri = format!("http://{redirect_address}/login");

let client = BasicClient::new(
ClientId::new(client_id.to_string()),
None,
AuthUrl::new("https://accounts.spotify.com/authorize".to_string()).unwrap(),
Some(TokenUrl::new("https://accounts.spotify.com/api/token".to_string()).unwrap()),
)
.set_redirect_uri(RedirectUrl::new(redirect_uri).expect("Invalid redirect URL"));

let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();

let scopes = vec![
"app-remote-control",
"playlist-modify",
"playlist-modify-private",
"playlist-modify-public",
"playlist-read",
"playlist-read-collaborative",
"playlist-read-private",
"streaming",
"ugc-image-upload",
"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-birthdate",
"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",
];
let scopes: Vec<oauth2::Scope> = scopes.iter().map(|&s| Scope::new(s.into())).collect();
let (auth_url, _) = client
.authorize_url(CsrfToken::new_random)
.add_scopes(scopes)
.set_pkce_challenge(pkce_challenge)
.url();

println!("Browse to: {}", auth_url);

let code = if redirect_port > 0 {
get_authcode_listener(redirect_address)
} else {
get_authcode_stdin()
};
debug!("Exchange {code:?} for access token");

let (tx, rx) = mpsc::channel();
let client_clone = client.clone();
std::thread::spawn(move || {
let resp = client_clone
.exchange_code(code)
.set_pkce_verifier(pkce_verifier)
.request(http_client);
tx.send(resp).unwrap();
});
let token_response = rx.recv().unwrap();
let token = match token_response {
Ok(tok) => {
trace!("Obtained new access token: {tok:?}");
tok
}
Err(e) => {
error!("Failed to exchange code for access token: {e:?}");
exit(1);
}
};

token.access_token().secret().to_string()
}
1 change: 1 addition & 0 deletions psst-core/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use self::{
pub struct SessionConfig {
pub login_creds: Credentials,
pub proxy_url: Option<String>,
pub client_id: String,
}

/// Cheap to clone, shareable service handle that holds the active session
Expand Down
22 changes: 17 additions & 5 deletions psst-gui/src/data/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,23 @@ pub enum PreferencesTab {
pub struct Authentication {
pub username: String,
pub password: String,
pub access_token: String,
pub result: Promise<(), (), String>,
}

impl Authentication {
pub fn session_config(&self) -> SessionConfig {
SessionConfig {
login_creds: Credentials::from_username_and_password(
self.username.to_owned(),
self.password.to_owned(),
),
login_creds: if !self.access_token.is_empty() {
Credentials::from_access_token(self.access_token.clone())
} else {
Credentials::from_username_and_password(
self.username.clone(),
self.password.clone(),
)
},
proxy_url: Config::proxy(),
client_id: Config::default().client_id,
}
}

Expand Down Expand Up @@ -95,6 +101,7 @@ pub struct Config {
pub sort_criteria: SortCriteria,
pub paginated_limit: usize,
pub seek_duration: usize,
pub client_id: String,
}

impl Default for Config {
Expand All @@ -113,6 +120,8 @@ impl Default for Config {
sort_criteria: Default::default(),
paginated_limit: 500,
seek_duration: 10,
// Default Spotify Desktop client ID
client_id: "65b708073fc0480ea92a077233ca87bd".to_string(),
}
}
}
Expand Down Expand Up @@ -184,13 +193,16 @@ impl Config {
}

pub fn username(&self) -> Option<&str> {
self.credentials.as_ref().map(|c| c.username.as_str())
self.credentials
.as_ref()
.map_or(None, |c| c.username.as_deref())
}

pub fn session(&self) -> SessionConfig {
SessionConfig {
login_creds: self.credentials.clone().expect("Missing credentials"),
proxy_url: Config::proxy(),
client_id: self.client_id.clone(),
}
}

Expand Down
1 change: 1 addition & 0 deletions psst-gui/src/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ impl AppState {
auth: Authentication {
username: String::new(),
password: String::new(),
access_token: String::new(),
result: Promise::Empty,
},
cache_size: Promise::Empty,
Expand Down
Loading

0 comments on commit cfd1f83

Please sign in to comment.