Skip to content

Conversation

@jacksongoode
Copy link
Collaborator

  • Make OAuth/PKCE primary
  • Depreciate Keymaster
  • Refresh tokens on demand

There are properly a handful of other things here. I didn't want to get rid of all the Keymaster stuff, but it might be that we should eventually do that too. I'd like to follow librespot on this and as I said before this is more of a bandaid. I think these problems ought to be resolved by integrating librespot entirely but that would be a big effort.

- Make OAuth/PKCE primary
- Depreciate Keymaster
- Refresh tokens on demand
@RensOliemans
Copy link

Just tested this locally (psst-x86_64-unknown-linux-gnu) and things work great, well done. Logging in, remembering token (I don't know how long a token persists, haven't seen refreshing in action yet probably), logging out, all works. And playback and navigation as well, for that matter :)

One thing is that the user information still shows "Disconnected" above the user name, this is probably wrong?

Additionally, ~/.config/Psst/config.json stores the bearer token and refresh token. Did psst store data unencrypted as well? I guess introducing secret services is out of scope for this PR, but just checking that this was also the case before.

Copy link

@RensOliemans RensOliemans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to ignore the review: I'm not well-versed in either rust or Psst's codebase and as such my comments are rather shallow.

@RensOliemans
Copy link

We're a day further, presumably my token has expired, and when restarting psst I get 401 errors. Logging out and back in works (well psst complained that another service was listening on 8888, when I restarted psst it worked fine, but I'm not sure if this problem was on my side or not, I'll check that next time)

@jacksongoode
Copy link
Collaborator Author

logging out and back in works (well psst complained that another service was listening on 8888, when I restarted psst it worked fine, but I'm not sure if this problem was on my side or not, I'll check that next time)

Right, in this PR there isn't any token refresh logic yet. Working on that now.

Copy link
Contributor

@Pogodaanton Pogodaanton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating OAuth is much neater than my idea!

};

// If unauthorized/forbidden, refresh once (if possible) and retry.
if matches!(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default, ureq sends a ureq::Error::StatusCode if the status code is an error. The refresh logic below this line will thus not be called, as the function errors out in line 128

@jacksongoode jacksongoode changed the title Fix depcreciated reliance on Keymaster tokens Fix depreciated reliance on Keymaster tokens Sep 24, 2025
@RensOliemans
Copy link

RensOliemans commented Sep 24, 2025

I downloaded the latest binary from https://github.com/jpochyla/psst/actions/runs/17954044878 (built after 2f7c543, I believe, psst-x86_64-unknown-linux-gnu, sha256:e36e29a0a0aa84bc86df42dc1bd544f449c398e300639aeb046b611bcb43521a) and the refresh token works perfectly, I did not have to re-authenticate.

clear_refresh_if_none: bool,
save: bool,
) {
log::info!("TokenUtils: persist save={save} clear_refresh_if_none={clear_refresh_if_none}");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these TokenUtil logs seem better suited to debug, right? I guess it is useful information to see when the token is refreshed. Aside: how do I change the logging level when running the binary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, for now while I'm testing I've included the logs. I think the compile removes them from the binary (though I'm not completely sure)...

@RensOliemans
Copy link

RensOliemans commented Sep 25, 2025

Hmm, after a while this happens:

[2025-09-25T18:02:35Z INFO  psst_core::player] pausing playback
[2025-09-25T19:23:01Z INFO  psst_core::player] resuming playback
[2025-09-25T19:23:03Z ERROR psst_core::player::file] failed to request audio range: OAuthError("Failed to refresh token")
[2025-09-25T19:23:05Z ERROR psst_core::player::file] failed to request audio range: OAuthError("Failed to refresh token")
[2025-09-25T19:23:06Z ERROR psst_core::player::file] failed to request audio range: OAuthError("Failed to refresh token")
[2025-09-25T19:23:09Z INFO  psst_core::player::file] blocked at 6619280
[2025-09-25T19:23:19Z ERROR psst_core::session] connection error: Os { code: 104, kind: ConnectionReset, message: "Connection reset by peer" }
[2025-09-25T19:23:19Z ERROR psst_core::player] skipping, error while loading: Session disconnected
[2025-09-25T19:23:19Z INFO  psst_core::connection] requesting AP list from http://apresolve.spotify.com
[2025-09-25T19:23:19Z INFO  psst_core::connection] received 6 APs from server
[2025-09-25T19:23:19Z INFO  psst_core::connection] successfully resolved 6 access points
[2025-09-25T19:23:19Z INFO  psst_core::connection] attempting to connect using 6 access points
[2025-09-25T19:23:19Z INFO  psst_core::connection] trying AP 1 of 6: ap-gew4.spotify.com:4070
[2025-09-25T19:23:19Z INFO  psst_core::connection] successfully connected to AP: ap-gew4.spotify.com:4070
[2025-09-25T19:23:19Z ERROR psst_core::player] skipping, error while loading: OAuth error: Failed to refresh token
[2025-09-25T19:23:19Z ERROR psst_core::player] stopping, error while loading: OAuth error: Failed to refresh token

If I then restart psst, I get the following logs:

[2025-09-25T19:27:09Z WARN  psst_gui] Failed to refresh OAuth token at startup: OAuth error: Failed to refresh token: Server returned error response. Falling back to persisted access token if any.
[2025-09-25T19:27:09Z INFO  psst_gui::token_utils] TokenUtils: install_from_config access=set, refresh=set
[2025-09-25T19:27:09Z INFO  psst_gui::token_utils] TokenUtils: runtime access=set, refresh=set
[2025-09-25T19:27:09Z ERROR psst_gui::webapi::client] failed to read local tracks: No such file or directory (os error 2)
[2025-09-25T19:27:10Z INFO  psst_core::audio::output::cpal] using audio device: "default"
[2025-09-25T19:27:10Z INFO  psst_core::cache] using cache: "/home/rens/.cache/Psst"
[2025-09-25T19:27:10Z INFO  psst_gui::controller::playback] Last.fm scrobbling is disabled, clearing Scrobbler instance.
[2025-09-25T19:27:10Z INFO  psst_core::audio::output::cpal] opening output stream: StreamConfig { channels: 2, sample_rate: SampleRate(44100), buffer_size: Default }
[2025-09-25T19:27:10Z INFO  psst_gui::webapi::client] Error loading home section: Failed to refresh token
[2025-09-25T19:27:10Z INFO  psst_gui::webapi::client] Error loading home section: Failed to refresh token
[2025-09-25T19:27:10Z INFO  psst_gui::webapi::client] Error loading home section: Failed to refresh token
[2025-09-25T19:27:10Z INFO  psst_gui::webapi::client] Error loading home section: Failed to refresh token
[2025-09-25T19:27:10Z INFO  psst_gui::webapi::client] Error loading home section: Failed to refresh token
[2025-09-25T19:27:10Z INFO  psst_gui::webapi::client] Error loading home section: Failed to refresh token
[2025-09-25T19:27:10Z INFO  psst_gui::webapi::client] Error loading home section: Failed to refresh token
[2025-09-25T19:27:10Z INFO  psst_gui::webapi::client] Error loading home section: Failed to refresh token

and I see "Error: failed to refresh token" in the GUI. Logging out and back in worked.

I'm not exactly sure why. I tried compiling psst locally, either with cargo build or cargo build --profile dev, but that doesn't seem to work.

@jacksongoode
Copy link
Collaborator Author

Yeah, I'm finding this as well. I don't know if this technique will actually work to keep refreshing for tokens... Grr. This approach could potentially be a dead end and login5 might be the solution?

@arch-btw
Copy link
Contributor

@jacksongoode feel free to ignore this if you don't think that's it but what about rotating the 6 access points that it fetches? They might be rate limited by access point. So for example if this one fails:

trying AP 1 of 6: ap-gew4.spotify.com:4070
OAuth error: Failed to refresh token

To try a different one?

@RensOliemans
Copy link

RensOliemans commented Sep 29, 2025

Hmm. I noticed that the refreshing didn't work. After some local testing, I'm pretty sure that spotify rotates their refresh token on each use. See f.e.:

$ python spotify-authorize.py > token.json                  # gets token from spotify
$ set -l refresh (jq -r '.refresh_token' token.json)        # fish' variant of export
$ curl -X POST https://accounts.spotify.com/api/token \
                                          -H "Content-Type: application/x-www-form-urlencoded" \
                                          -d "grant_type=refresh_token&refresh_token=$refresh&client_id=<hidden>" \
                                          -s                # first call, all good
{"access_token":"<hidden>", "token_type":"Bearer","expires_in":3600,"refresh_token":"<hidden>","scope":"playlist-read-private playlist-read-collaborative user-follow-read playlist-modify-private user-read-email user-read-private app-remote-control streaming user-follow-modify user-library-read user-library-modify playlist-modify-public user-read-recently-played user-top-read"}
$ curl -X POST https://accounts.spotify.com/api/token \
                                          -H "Content-Type: application/x-www-form-urlencoded" \
                                          -d "grant_type=refresh_token&refresh_token=$refresh&client_id=<hidden>" \
                                          -s                # second, identical call, no good
{"error":"invalid_grant","error_description":"Refresh token revoked"}

This means that everywhere we call oauth::refresh_access_token(), we have to write it the result to Config. Currently TokenUtils does this, but this isn't used by psst-core, right? I'm learning the codebase bit by bit, please correct me if I'm wrong.

@jacksongoode
Copy link
Collaborator Author

Closed in favor of #693

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants