librespot icon indicating copy to clipboard operation
librespot copied to clipboard

Credentials with access token (oauth)

Open kingosticks opened this issue 1 year ago • 31 comments

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 avatar Aug 06 '24 13:08 kingosticks

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.

kingosticks avatar Aug 14 '24 11:08 kingosticks

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,                                                                                                                                                                                  
        };

MarvAmBass avatar Aug 15 '24 17:08 MarvAmBass

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

kingosticks avatar Aug 15 '24 18:08 kingosticks

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

MarvAmBass avatar Aug 15 '24 18:08 MarvAmBass

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.

kingosticks avatar Aug 15 '24 18:08 kingosticks

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?

MarvAmBass avatar Aug 15 '24 18:08 MarvAmBass

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

kingosticks avatar Aug 15 '24 18:08 kingosticks

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!

MarvAmBass avatar Aug 15 '24 18:08 MarvAmBass

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

dbalague avatar Aug 15 '24 20:08 dbalague

Just quickly tried out the changes in this PR but it doesn't work when using a token that was created using my own PKCE flow. So imagine you have your own (app specific) PKCE oauth flow to get the access token and pass that to librespot. It will fail on what I believe is a missing scope.

[2024-08-16T09:47:39Z INFO  librespot_core::session] Connecting to AP "ap-gew4.spotify.com:4070"
[2024-08-16T09:47:39Z INFO  librespot_core::session] Authenticated as '#######' !
[2024-08-16T09:47:39Z INFO  librespot_core::session] Country: "##"
[2024-08-16T09:47:39Z INFO  librespot_core::spclient] Resolved "gew4-spclient.spotify.com:443" as spclient access point
[2024-08-16T09:47:39Z ERROR librespot_core::mercury] error 403 for uri hm://keymaster/token/authenticated?scope=playlist-read&client_id=65b708073fc0480ea92a077233ca87bd&device_id=ce8d71004f9597141d4b5940bd1bb2dc52a35dae
[2024-08-16T09:47:39Z ERROR librespot_playback::player] Unable to load audio item: Error { kind: Unavailable, error: Response(MercuryResponse { uri: "hm://keymaster/token/authenticated?scope=playlist-read&client_id=65b708073fc0480ea92a077233ca87bd&device_id=ce8d71004f9597141d4b5940bd1bb2dc52a35dae", status_code: 403, payload: [[123, 34, 99, 111, 100, 101, 34, 58, 52, 44, 34, 101, 114, 114, 111, 114, 68, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 34, 58, 34, 73, 110, 118, 97, 108, 105, 100, 32, 114, 101, 113, 117, 101, 115, 116, 34, 125]] }) }

EDIT: nvm figured out that I also had to merge in the changes to use login5. Seems to be working fine now using a token created by a PKCE auth flow, passed as token to librespot.

marcelveldt avatar Aug 16 '24 09:08 marcelveldt

Yes, I tried to explain this in the first message. I think we can sort that out in another PR.

kingosticks avatar Aug 16 '24 12:08 kingosticks

Hopefully not too out of context for the scope of this PR but if I understood @roderickvd correctly, then moving to the HTTP-based auth mechanism would also enable using librespot in the browser via WASM? (Any additional context on this would be appreciated)

schickling avatar Aug 16 '24 17:08 schickling

I have tested this pull request and found a little bug. There is an http server involved used to get the token code during the authentication flow with --token "". Librespot crashes when http://127.0.0.1:5588/ is opened without the redirect code.

zaciam avatar Aug 16 '24 21:08 zaciam

Hopefully not too out of context for the scope of this PR but if I understood @roderickvd correctly, then moving to the HTTP-based auth mechanism would also enable using librespot in the browser via WASM? (Any additional context on this would be appreciated)

I don't think so, not until someone comes up with an original (i.e. non-decompiled) version of the playplay HTTP endpoint that gets the track decryption keys. Without that, we still require TCP-based Mercury to get the keys, and that won't work in WASM right? No WASM expert.

I have tested this pull request and found a little bug. There is an http server involved used to get the token code during the authentication flow with --token "". Librespot crashes when http://127.0.0.1:5588/ is opened without the redirect code.

Anyone who can confirm and/or fix this?

Back from holidays, starting my review now.

roderickvd avatar Aug 21 '24 19:08 roderickvd

Sorry, hit the wrong button adding all the review comments individually. Let me know if this is acceptable for you to work on before merging. Would also be great if you could add to the changelog.

roderickvd avatar Aug 21 '24 19:08 roderickvd

Sure, I'm currently on holiday but will do on my return next week.

kingosticks avatar Aug 21 '24 20:08 kingosticks

I don't think so, not until someone comes up with an original (i.e. non-decompiled) version of the playplay HTTP endpoint that gets the track decryption keys. Without that, we still require TCP-based Mercury to get the keys, and that won't work in WASM right? No WASM expert

I'm not a wasm expert either, but I think my workaround should work in WASM (I haven't tested it WASM, but I'm pretty sure), but I'm not going to publish it until it becomes critical or spotify gets lossless support : (

SuisChan avatar Aug 22 '24 03:08 SuisChan

I hope this isn't just noise and may be of help. But a while back I implemented OAuth for the Spotify API in Rust targeting WASM I never got around to doing anything with it so I'll leave it here for reference. If you find it particularly helpful, I would appreciate attribution.

G-M0N3Y-2503 avatar Aug 24 '24 05:08 G-M0N3Y-2503

I'm not a wasm expert either, but I think my workaround should work in WASM (I haven't tested it WASM, but I'm pretty sure), but I'm not going to publish it until it becomes critical or spotify gets lossless support : (

I would advise anyone not to hold their breath on this. dude has been teasing this literally for years:

https://github.com/librespot-org/librespot-java/discussions/421

so @SuisChan either post the code or stop talking about it.

3052 avatar Aug 24 '24 15:08 3052

I'm not a wasm expert either, but I think my workaround should work in WASM (I haven't tested it WASM, but I'm pretty sure), but I'm not going to publish it until it becomes critical or spotify gets lossless support : (

I would advise anyone not to hold their breath on this. dude has been teasing this literally for years:

librespot-org/librespot-java#421

so @SuisChan either post the code or stop talking about it.

You know, this is a bit funny. but let's make this clear. like I said, this thing is real, there's an old version (clone) on GitHub somewhere (my orig was wiped out link).

And like I said, I won't share it unless it's really necessary (completely broken/disconnected tcp connection or something like that), I need a reason to share the decryptor, ok? I won't repeat it again. Period.

You can say whatever you want, it won't change anything. 🤷

SuisChan avatar Aug 24 '24 15:08 SuisChan

Let's keep it constructive. I've seen the code before the link was deleted because Spotify told us to. And they could because it was a decompiled version, which is not allowed. Best we can do is come up with an original implementation that's not their IP.

roderickvd avatar Aug 24 '24 15:08 roderickvd

people will just sit around waiting for you to release it, rather than making something themself

  1. Code was published before, believe it or not.
  2. Yes, because I spent my time on it, so it's up to me to decide when to publish, no?

SuisChan avatar Aug 24 '24 15:08 SuisChan

Let's keep it constructive. I've seen the code before the link was deleted because Spotify told us to. And they could because it was a decompiled version, which is not allowed. Best we can do is come up with an original implementation that's not their IP.

you wanna be constructive, then post the decompiled code. if GitHub dont allow it, then post somewhere else and link to it here. I would argue that talking about mystery code is highly unconstructive, so lets make it concrete. if the decompiled code is made widely available, then people can use it to come up with original implementations.

3052 avatar Aug 24 '24 15:08 3052

I think ideally I'd move this out of get_setup() and into main(). I think it makes more sense to start a server there and it'd simplify the diff a bit.

kingosticks avatar Aug 29 '24 23:08 kingosticks

@kingosticks, you mention in the get_token example that the initial session created with the token wouldn't work with keymaster. Wouldn't it make more sense to add the "workaround" or solution to that problem into the connect method of Session?

Due to handling that case only in the get_token example, starting librespot with a token will not work unless you cache the session and restart librespot with the cached session.

photovoltex avatar Aug 31 '24 20:08 photovoltex

@photovoltex yep you are totally right. When I first made this PR I also implemented the login5 side of things, and replaced the token master calls. I then removed all that in an attempt to make this PR smaller and simpler, which has exposed that issue again.

kingosticks avatar Sep 02 '24 21:09 kingosticks

By chance, is it normal for playback to not work when using my own client_id? I'm requesting the same scopes but for whatever reason I'm only getting 403 forbidden calling AudioItem::get_file() when using my own client_id, but the same works totally fine while using the official client's ID.

SilverMira avatar Sep 04 '24 15:09 SilverMira

I think it's buried in the comments here but essentially, if you're using our default list of scopes then that is normal. The default scopes we are requesting here are the same as what the desktop client requests. It seems at least one of the scopes isn't allowed for client IDs other than Spotify's. If you trim it down to just "streaming" you should be able to use your own client ID, I think I tested that... maybe you can reconfirm? Ideally we'd work out what's the minimum subset of universally allowed scopes and what's needed to make everything in librespot work, but that's outside my interest.

I hate GitHub's hiding comments "feature".

kingosticks avatar Sep 04 '24 16:09 kingosticks

If you trim it down to just "streaming" you should be able to use your own client ID, I think I tested that... maybe you can reconfirm?

Turns out setting SessionConfig::client_id to my own client_id was what resulted in the Forbidden 403 when calling AudioItem::get_file, I thought at first SessionConfig::client_id must be the same client_id as what was used in the oauth flow? Still, no idea whether this behavior is expected or not.

Now what works for me:

  1. OAuth flow with only scope "streaming" using my own client_id
  2. Get credentials with Credentials::with_access_token()
  3. Create and use session2 as the playback session (session doesn't work either), noting that SessionConfig::client_id must use the official's ID (set by default from SessionConfig::default()), following the example https://github.com/librespot-org/librespot/blob/5093a88e5f7162d2a453c005c60530abaf9bcc47/examples/get_token.rs#L22-L50
  4. AudioItem::get_file and friends works, able to stream audio data

SilverMira avatar Sep 05 '24 02:09 SilverMira

Thanks for clarifying. Yes, that is expected. Maybe I didn't explain well before but I can see how the example is confusing. Essentially, you can use any client ID you want in step 1, it's a regular Spotify OAuth flow as per their public developer documentation. But you must use one of Spotify's client IDs when creating a librespot Session (and it doesn't matter that your access token originally came from a different client ID). Your client ID doesn't have the required permissions to access their internal APIs. I will try and clarify the example and the OAuth module docs.

kingosticks avatar Sep 05 '24 08:09 kingosticks