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

Conversation

kingosticks
Copy link
Contributor

@kingosticks kingosticks commented Aug 6, 2024

I don't know if this oauth stuff really belongs in core, it doesn't feel quite right there so I added a new module. That new module could be useful standalone so it makes sense. If someone wants to take this and do something else that is fine by me.

This also leaves the token stuff a bit messy. We now provide two ways to get an access token:

  1. session.token_provider().get_token("your,scopes") using keymaster (Mercury)
  2. session.spclient().auth_token() using login5 (HTTP)

Both methods work (for session auth and playback) when you authenticate your session using a password or stored credentials. However, method 1 doesn't work when you authenticate your session using a spotify token (obtained using either method).

I think we want to get rid of this annoying pitfall. We could:
a) Get rid of method 1 altogether
b) Method 1 use method 2 under the hood
c) Change session authentication so when stored-creds are not used, it auths to obtain them and then re-auths using them.

Fixes #1308

@kingosticks
Copy link
Contributor Author

I pulled out the login5 stuff from here since it's not actually required to implement the oauth login flow. But I did leave in the session.auth_data() hook required to make that work at a later date. Should make this easier to merge.

Isn't required for token auth.
We might need this later if need to re-auth and original creds are
no longer valid/available.
Sometimes there is also a username field returned with the token, but not
always. It's nice to have but not needed (since we'll get it when we auth
our session) and trying to extract it requires lots of boilerplate from
the oauth lib. Let's keep it simple.
Provide a token with sufficient scopes or empty string to obtain new
token.

When obtaining a new token, use --token-port argument to specify the
redirect port. Specify 0 to manually enter the auth code (headless).

Re-arranged setup function so have session_config earlier for use with
get_access_token().
@MarvAmBass
Copy link

I've just tested this, unfortunately your example also exits with

Connecting with password..
Error connecting: Permission denied { Login failed with reason: Bad credentials }

also (I'm new to rust so maybe this is wrong) I run into the following compile error and had to change your code to

        let unknown = "UNKNOWN".to_string();                                                                                                                                                                
        let username = match reusable_credentials.username.as_ref() {                                                                                                                                       
            Some(username) => username,                                                                                                                                                                     
            _ => &unknown,                                                                                                                                                                                  
        };

@kingosticks
Copy link
Contributor Author

If it mentions password then that's not using --token mode. Sorry about the bad compile, I was trying to improve it last night and then GitHub went down leaving it in a mess. I'll sort that out later hopefully

@MarvAmBass
Copy link

anyway thanks for your work! hope we get spotify working soon :)

@kingosticks
Copy link
Contributor Author

kingosticks commented Aug 15, 2024

Oh, and yes sorry, you meant the actual example. Yes, that still needs updating. I was only using that for testing (before they deprecated password).

Thank you for trying it though. I've had very little feedback otherwise.

@MarvAmBass
Copy link

As I understood your code, the examples/get_token.rs is not yet using your oauth module. no wonder it didn't work.

regarding the OAuth way, does every user need to register a client?! or do we fake/emulate the spotify app and force spotify to redirect to localhost?

@kingosticks
Copy link
Contributor Author

kingosticks commented Aug 15, 2024

We can keep using Spotify's desktop client ID and either pop in our own redirect Uri and do it like them, or not bother and just have the redirect fail (harmless) but then the user has to manually provide the Auth code back to our code somehow. If you run librespot in this PR you can see both modes:

cargo run --no-default-features -- --cache . --token ""

And

cargo run --no-default-features -- --cache . --token "" --token-port 0

Yes, the redirect host has to be 127.0.0.1 when using their client ID. Anything else errors.

If you do want to use your own client ID then that's also possible (not exposed in this PR) but then you've got to alter the scopes since it appears some of the ones they're using are not universally available. I don't know if the scopes you ask for here beyond 'streaming' actually matter, and how they impact what you can later request an access token for. E.g. if I Auth the session with just 'streaming' scope, can I later get an access token for more scopes? Presumably not but I have not tested

@MarvAmBass
Copy link

MarvAmBass commented Aug 15, 2024

ohhh nice thanks for the hint it seems that

cargo run --no-default-features -- --cache . --token ""

works as expected! it simulates a spotify desktop app and recieves the token - thanks for this!

@dbalague
Copy link

Tried it on a Raspberry Pi 5 with cargo 1.80.1 and it worked like a charm!

@kingosticks kingosticks force-pushed the credentials_with_access_token branch 2 times, most recently from 7d776cc to bf8b003 Compare August 16, 2024 00:34
@spockfish
Copy link

Hi @kingosticks,

First of all: thanks for your work! I think that everyone running Librespot was waiting for this to fix the authentication stuff.

With respect to 4: would it also be an idea to be able to provide the redirect url manually? I'm aware that the user then has the responsibility to provide the correct one of course, but for the 'remote' situation this might be nice...

Anyways, again thanks for fixing the authentication!

@spockfish
Copy link

Question: everything is working, except that I can't see/find where the token is stored for the next time. So right now I need to repeat the procedure whenever I start librespot.

@kingosticks
Copy link
Contributor Author

With the exception of the port, there isn't actually a choice when it comes to what redirect Uri to use, it has to match what's registered for the particular client ID being used. I think --oauth-port 0 for headless/remote users should be adequate.

The token isn't stored. The token is exchanged for Spotify stored credentials, which are then saved in the cache folder as usual. You should run with --cache <some-folder>. I'm not sure why caching isn't enabled by default.

@kingosticks
Copy link
Contributor Author

kingosticks commented Sep 14, 2024

@roderickvd what do you think about making the the system-cache enabled by default in 0.5? Having to do the oauth flow every time is bonkers and it shouldn't be the default.

Personally I'd go the whole hog and have all caching enabled by default.. anyone who cares should be able to handle the change. And those that don't care can enjoy better performance. But I don't want to have a big discussion on the data cache here, I'll make a new issue if there's interest in that part.

@Losses
Copy link

Losses commented Sep 14, 2024

@kingosticks What about print a large warning when cache directory is not available to make users aware of what are they doing?

@roderickvd
Copy link
Member

Makes sense to me, to enable it by default. Would you put in a change?

@kingosticks kingosticks deleted the credentials_with_access_token branch September 16, 2024 15:16
@spockfish
Copy link

spockfish commented Sep 21, 2024

I think --oauth-port 0 for headless/remote users should be adequate.

@kingosticks Challenge for me is that I provide Librespot as part of an appliance (RoPieee) that does not require any kind of (linux) knowledge to get things rolling.

And right now I don't have a clue how to create an authorization feature that's still relatively easy to do for non-geeks like us...

I tried this by creating a small reverse proxy and change the port number of the redirect uri... but that fails. The redirection and all works, but I get an error message from Librespot.

Maybe I'm overlooking something here (or I'm misunderstanding things), but I appreciate your thoughts on this...

@kingosticks
Copy link
Contributor Author

kingosticks commented Sep 21, 2024

Ideally your users would be able to copy and paste the failing redirect Uri into whatever admin console you presumably already have. You can then start librespot with --token. I don't believe there is any way to highjack the redirect within the browser itself, that's no accident.

Otherwise, reimplement the oauth handler on your own domain with your own client ID, request streaming scope, and display the resulting access token for the user to then paste into your admin console as above. A lot more hassle for a less ugly flow, but still essentially copying something from one window to another. This also requires you to get your own Spotify client ID and last time I checked, Spotify were hopeless at approving these.

Or maybe your users can auth using Spotify connect? Ideally we'd be able to provide the device flow but I don't know if anyone reversing this https://community.spotify.com/t5/Spotify-for-Developers/Device-Authorization-Grant-authentication-flow-for-custom/td-p/5485468

@SilverMira
Copy link

Just an idea: I was sort of able to hijack the redirect by spawning a WebView of my control to the oauth url and listening for navigation to the callback url, this works even if callback urls are custom protocol like spotify-auth:// on android.

This depends if you have your own "frontend" configuration app though

@kingosticks
Copy link
Contributor Author

kingosticks commented Sep 22, 2024

That works because you are listening for the redirect on the same machine where the redirect is happening. In the headless case, you simply cannot do that with just a browser on the user's machine. It should also go without stating that that ssh forwarding etc would also be non-starters. Maybe we should start a dedicated discussion on this topic.

@kingosticks
Copy link
Contributor Author

Personally I'd go the whole hog and have all caching enabled by default.. anyone who cares should be able to handle the change. And those that don't care can enjoy better performance. But I don't want to have a big discussion on the data cache here, I'll make a new issue if there's interest in that part.

I've been thinking more about this and I'm less opinionated now, to the point I'm not going to bother. To have it default enabled you need a default location. Could be the cwd or could be the XDG dir. Then you need the (existing) option to specify somewhere else and a new option to disable it entirely. And that's got to play nice with the general cache directory too. This is all more work and it's not interesting work to me, I've enough boring work to do as it is. I'll add a warning as suggested, and be done with it.

@TDethlefs
Copy link

OAuth Authentication

I used a third Option with Curl:

  1. Run the following command on the remote device to initiate the OAuth process:
    librespot --cache YOUR_CONFIGURATION_PATH -j
  2. Open the provided URL in a web browser to complete the login process until you are redirected to a "127.0.0.1:5588..." URL that cannot be opened. Copy that URL, SSH-Login to your remote Device and run curl with that URL:
curl "http://127.0.0.1:5588/login?code=AQC..."

The double quotes around the URL after "curl" are important as the URL includes a "&" that is interpreted by the shell if not in double quotes.

Good Luck.

@kingosticks
Copy link
Contributor Author

Which is like using --oauth-port 0 and pasting in your redirect Uri when prompted, but adds using curl also. I'm not convinced that adding more software to the process helps but whatever works for you.

@armanhi
Copy link

armanhi commented Oct 2, 2024

Quick question: how is the user session refreshed/kept on librespot? I can see the new OAuth "get_access_token" method returns a refresh_token, but it doesn't seem to be used anywhere in the application.

@kingosticks
Copy link
Contributor Author

We don't refresh our access token. We use it to login and obtain reusable credentials. After that there's no longer any need for the access token. And therefore no requirement to use the refresh token. If we need another access token later, we can get one using our session.

You could keep the access token and refresh it to keep it valid. It should be possible to do that using the oauth2 library, similarly to how librespot-oauth used it to obtain the tokens in the first place. But that's an exercise for someone interested since it's not directly useful for librespot.

@armanhi
Copy link

armanhi commented Oct 2, 2024

Thank you for your reply!

So, if I understand it correctly, the access token lasts indefinitely? I've always assumed it should be refreshed. Also, the Spotify docs (https://developer.spotify.com/documentation/web-api/tutorials/code-flow) says that the "https://accounts.spotify.com/api/token" response contains a "expires_in" field, meaning "The time period (in seconds) for which the access token is valid.". That's where my question comes from.

I'm asking mainly because of something that happened to me the other day. I've generated a credentials.json file using zeroconf from librespot-auth (https://github.com/dspearson/librespot-auth), copied it to another machine and, using spotify-player (https://github.com/aome510/spotify-player), after a while, probably 1 hour, I started getting the "Bad credentials" error, so I thought the reason was the 3600s duration returned when the access token is obtained. Maybe the zeroconf expires?

Just one more question: does the credentials.json file support the "https://accounts.spotify.com/api/token" response I mentioned above (containing the access_token directly) or just the zeroconf json contract?

@kingosticks
Copy link
Contributor Author

kingosticks commented Oct 2, 2024

No, all Spotify access tokens expire. The token you get back literally has an expiry field, as you say.

Once we login using a token, we can request a credential blob aka reusable credentials aka stored credentials. This blob does not expire and can be used instead of an access token next time.

I don't really understand the other part of your question but I believe the Zeroconf auth blob is something different:

The encrypted blob received in this ZeroConf message must not be saved. On a successful login, the SpCallbackConnectionNewCredentials() callback will be invoked with the credentials to save to persistent storage.

https://developer.spotify.com/documentation/commercial-hardware/implementation/guides/zeroconf

I don't remember much about the Zeroconf stuff, I don't use it. If you have questions or problems with it, start a new discussion post so others can see it and help newer.

@kingosticks
Copy link
Contributor Author

kingosticks commented Oct 2, 2024

But I should also add, I don't know exactly what librespot-auth does. It's always been possible to produce a credentials.json file via zeroconf using regular librespot. And I'm not familiar with 'spotify-player', you should probably ask them why their software doesn't work as you'd like.

@armanhi
Copy link

armanhi commented Oct 2, 2024

I mean, they do use librespot under the hood, but since Spotify's change I definitely understand it's kinda tough to keep everything working as usual.

Is it possible to pass a valid access token as argument to librespot? Should the credentials.json file work if I have a json with the format
{ "access_token":"ACCESS_TOKEN", "token_type":"Bearer", "expires_in":3600, "refresh_token":"REFRESH_TOKEN", "scope":"SCOPES_LIST"} ?

@kingosticks
Copy link
Contributor Author

Yes, use --help to see the argument name.

No, that won't work. Credentials.json should be created for you by librespot.

@armanhi
Copy link

armanhi commented Oct 3, 2024

Awesome, thank you so much!

@kingosticks
Copy link
Contributor Author

I (finally) added some missing option wiki documentation and clarification. I will try to add something to the authentication wiki page at some point.

aome510 added a commit to aome510/spotify-player that referenced this pull request Oct 28, 2024
closes #520 
closes #579 
closes #580 

- upgrade dependencies. Main change involves the migration to `librespot v0.5.0`
- migrate authentication workflow to OAuth implemented in (librespot-org/librespot#1309)

## Next step
- handle Spotify Connect with user-provided `client_id`

Co-authored-by: Julia Mertz <[email protected]>
Co-authored-by: Thang Pham <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Authentication failures