brim-borium / spotify_sdk

Flutter Package to connect the spotify sdk
https://pub.dev/packages/spotify_sdk
Apache License 2.0
143 stars 81 forks source link

Web takes optional tokenSwapURL and tokenRefreshURL for Authorization Code (without PKCE) and token swap #121

Closed nzoschke closed 2 years ago

nzoschke commented 2 years ago

Before, a Web Playback SDK developer could only use the internal in PKCE auth and refresh flow, which seems to always require an auth prompt.

Now, a developer can specify tokenSwapURL and tokenRefreshURL. If specified the library performs and Authorization Code (without PKCE) flow, and uses the swap and refresh URLs to exchange the code for a token, and to refresh the token respectively.

The token swap pattern is a first-class pattern on iOS, but the same concept applies to the web SDK for Spotify apps that manage access and refresh tokens with a server side component.

https://developer.spotify.com/documentation/ios/guides/token-swap-and-refresh/

nzoschke commented 2 years ago

I am testing this with something similar to the below. It appears to be working well with:

  Future init() async {
    var apiURI = 'https://jukelab.herokuapp.com';
    var clientID = '<REDACTED>';
    var redirectURI = 'http://localhost:8686/callback.html';

    SpotifySdkPlugin.tokenSwapURL = '$apiURI/api/v1/spotify/token';
    SpotifySdkPlugin.tokenRefreshURL = '$apiURI/api/v1/spotify/refresh';

    var scope = [
      'app-remote-control',
      'playlist-modify-public',
      'playlist-read-collaborative',
      'playlist-read-private',
      'streaming',
      'user-library-modify',
      'user-library-read',
      'user-modify-playback-state',
      'user-read-currently-playing',
      'user-read-email',
      'user-read-playback-state',
      'user-read-private',
      'user-top-read',
    ].join(',');

    var token = await SpotifySdk.getAuthenticationToken(
      clientId: clientID,
      redirectUrl: redirectURI,
      scope: scope,
    );

    client = SpotifyClient(bc.BrowserClient(), token);

    var ok = await SpotifySdk.connectToSpotifyRemote(
      clientId: clientID,
      redirectUrl: redirectURI,
      playerName: 'JukeLab',
      scope: scope,
    );

    print('connect $ok');
  }
nzoschke commented 2 years ago

Something new to consider is that it would be nice if I could get the underlying access token to use for the Spotify API.

getAuthenticationToken still returns a token which works, but I don't have a way to refresh it without duplicating and competing with the internal use of tokenRefreshURL.

Would you consider a getAccessToken helper?

fotiDim commented 2 years ago

Just a disclaimer, on iOS there getAuthenticationToken & connectToSpotifyRemote methods do the exact same thing under the hood only difference being that getAuthenticationToken actually returns the token. So in a nutshell on iOS, getAuthenticationToken = connectToSpotifyRemote + return token. On web it is very similar and frankly if it wasn't for Android we would have unified those 2 methods.

Now having said all that, I would like to understand the suggestion a bit better. Do you need a getAccessToken method that just returns the already generated access token without authenticating the user again?

Can you elaborate on I don't have a way to refresh it without duplicating and competing with the internal use of tokenRefreshURL.?

nzoschke commented 2 years ago

Just a disclaimer, on iOS there getAuthenticationToken & connectToSpotifyRemote methods do the exact same thing under the hood only difference being that getAuthenticationToken actually returns the token.... On web it is very similar

Thanks for the extra info about iOS. I don't see the similarity on web. Both getAuthenticationToken and connectToSpotify call _authorizeSpotify under the hood, butgetAuthenticationToken` doesn't do anything to initialize the Web Playback player or anything.

Now having said all that, I would like to understand the suggestion a bit better. Do you need a getAccessToken method that just returns the already generated access token without authenticating the user again?

Yes exactly.

The use case is to use the spotify package or similar to make Web API calls in parallel with the SDK.

I'd like to keep my web API client and the Web Playback SDK in sync and use the same accessToken and refresh mechanism.

Otherwise there's yet another token flow for me to manage, and I fear one would refresh without the other knowing and things would get out of sync.

So for every API call I'd like to be able to use SpotifySdk.accessToken or similar.

But thinking through that, then I'd also like some refresh mechanism, either SpotifySdk.refreshToken() to call if I get a 4XX from the API, or to get the accessToken thru an async getter that checks the expiry and refreshes it.

In a nutshell after this change and the web SDK has the access token and refresh built in, I'd like to reuse that to make Web API calls.

However I'm fine to punt on this and focus on this first bit just for the token swap.


EDIT: the way in the internal web client is doing 'Authorization': 'Bearer ${await _getSpotifyAuthToken()}' is exactly what I want to do externally too

fotiDim commented 2 years ago

Thanks for the extra info about iOS. I don't see the similarity on web. Both getAuthenticationToken and connectToSpotify call _authorizeSpotify under the hood, butgetAuthenticationToken` doesn't do anything to initialize the Web Playback player or anything.

Let me put it this way, getAuthenticationToken and connectToSpotify are a bit overlapping on iOS and web. We could have gotten away by only having one of those two and going through the entire initialization. Then an additional getter for the access token would have been enough.

I'd like to keep my web API client and the Web Playback SDK in sync and use the same accessToken and refresh mechanism.

I do something similar and I leave this library do the refreshing and then update the accessToken on the spotify package.

So for every API call I'd like to be able to use SpotifySdk.accessToken or similar.

I agree that this is missing. My issue is that getAuthenticationToken semantically overlaps and it will create confusion. Let's discuss that in a new issue or PR.

But thinking through that, then I'd also like some refresh mechanism, either SpotifySdk.refreshToken() to call if I get a 4XX from the API, or to get the accessToken thru an async getter that checks the expiry and refreshes it.

Can be arranged. On the native iOS SDK there is the reniewSession() call that does exactly that.