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
@kingosticks
Copy link
Contributor Author

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.

@photovoltex
Copy link

@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.

@kingosticks
Copy link
Contributor Author

@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.

@SilverMira
Copy link

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.

@kingosticks
Copy link
Contributor Author

kingosticks commented Sep 4, 2024

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".

Added --enable-oauth / -j option.
Using --password / -p option will error and exit.
@SilverMira
Copy link

SilverMira commented Sep 5, 2024

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
    // Now create a new session with that token.
    let session = Session::new(session_config.clone(), None);
    let credentials = Credentials::with_access_token(access_token);
    println!("Connecting with token..");
    match session.connect(credentials, false).await {
    Ok(()) => println!("Session username: {:#?}", session.username()),
    Err(e) => {
    println!("Error connecting: {e}");
    return;
    }
    };
    // This will fail. You can't use keymaster from an access token authed session.
    // let token = session.token_provider().get_token(SCOPES).await.unwrap();
    // Instead, derive stored credentials and auth a new session using those.
    let stored_credentials = Credentials {
    username: Some(session.username()),
    auth_type: AuthenticationType::AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS,
    auth_data: session.auth_data(),
    };
    let session2 = Session::new(session_config, None);
    match session2.connect(stored_credentials, false).await {
    Ok(()) => println!("Session username: {:#?}", session2.username()),
    Err(e) => {
    println!("Error connecting: {}", e);
    return;
    }
    };
  4. AudioItem::get_file and friends works, able to stream audio data

@kingosticks
Copy link
Contributor Author

kingosticks commented Sep 5, 2024

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.

Token authenticated sessions cannot use keymaster. So reconnect using
the reusable credentials we just obtained. Can perhaps remove this
workaround once keymaster is replaced with login5.
@kingosticks
Copy link
Contributor Author

This is in a better state now. Still zero tests (sorry, World) but unwrap-free and slightly better documentation.

One bit I still don't like is my OAuthToken type. I didn't want to return the oauth crate token since it's overly complex to use and I'd rather own the API interface types. I didn't want to pull in our existing token type from core because that adds an otherwise unnecessary dependency. Maybe I should provide a from<OAuthToken> implementation in core.

Arguably all the newer token stuff in core could be pulled out into a useful standalone crate but I don't quite see the point (yet).

Copy link
Member

@roderickvd roderickvd left a comment

Choose a reason for hiding this comment

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

I found two more unwraps, if you could fix those?

oauth/src/lib.rs Outdated Show resolved Hide resolved
oauth/src/lib.rs Outdated Show resolved Hide resolved
oauth/src/lib.rs Outdated Show resolved Hide resolved
oauth/src/lib.rs Show resolved Hide resolved
Copy link
Member

@roderickvd roderickvd left a comment

Choose a reason for hiding this comment

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

Even with tests! Perfect 😁

@roderickvd
Copy link
Member

Perfect. I am going to merge this now. I don't have time for a release this weekend, so let's target next week for that. That leaves room coming days to give this a good shake-down, and putting in any last-minute PRs.

There are a couple of almost-finished good PRs in there, would be nice if they could get polished in time for release.

@roderickvd roderickvd merged commit 4f9151c into librespot-org:dev Sep 13, 2024
9 checks passed
@Losses
Copy link

Losses commented Sep 13, 2024

Wow, is there a detailed document about how to use this for now?

@Losses
Copy link

Losses commented Sep 13, 2024

Okay, a quick note for users who is feeling confusing about this:

OAuth Authentication

Password login has been deprecated and is no longer supported. Instead, OAuth authentication must be used. Follow the steps below to authenticate using OAuth:

  1. Run the following command to initiate the OAuth process:

    librespot --cache YOUR_CONFIGURATION_PATH -j
  2. If there is no token in the specified directory, you will see a log similar to this:

    [INFO  librespot] librespot 0.5.0-dev b68516c (Built on 1980-01-01, Build ID: 315532800, Profile: debug)
    Browse to: https://accounts.spotify.com/authorize?response_type=code&client_id=...
    

    Note: This URL contains sensitive information. Please be cautious and avoid sharing it publicly.

  3. Open the provided URL in a web browser to complete the login process.

  4. If your device is remote, you have two options:

    • Complete the login process locally and then copy the token directory to the remote location, or use sshfs to mount it remotely.
    • Use port forwarding to complete the login process remotely, you can see the port need to be forwarded from the log:
    [INFO  librespot_oauth] OAuth server listening on 127.0.0.1:5588
    

Good luck.

@kingosticks
Copy link
Contributor Author

kingosticks commented Sep 13, 2024

I'll update the wiki and any other documentation I can find regarding auth. I need to somehow clarify this is different between what's released and what's in dev, confusion is probably inevitable for a while... sorry about that. I might leave some for posterity (clearly marked as deprecated).

Regarding point 2 above, i don't believe there's anything fundamentally sensitive in that URL, particularly as we're using Spotify's client ID which is easy to discover and use. But you raise a great point and there is sensitive info in the trace messages that follow as it displays the secret token values. When I was developing this I found that detail invaluable, but now there's an example in the oauth module which can provide the same detail, so we could redact the logging here.

For point 4, there's an alternate mode for this situation where instead of spawning a server to accept the response URI, librespot instead prompts you to manually enter that response URI. This means you can complete the oauth dance on your local machine and when the redirect fails (because nothing is listening on your local machine), just copy and paste the URL into librespot's prompt. There's a few ways to activate this mode, the easiest is to use option --oauth-port 0 to NOT specify a port. I wanted to avoid adding yet another option to explicitly control this mode but I'm not sure where I ended up is the nicest, even once it's documented.

Edit: maybe something like --enable-oauth [server|terminal], with the default being 'server' would have been a nicer UI. If we were to make that change, we should do it ASAP.

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