-
Notifications
You must be signed in to change notification settings - Fork 81
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
Web SDK can take a custom OAuth token handler function #116
Conversation
I'm splitting up #114 into smaller parts. This addresses No 2 from #114 (comment)
I tested that the standard usage of this library still works with: {
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,
);
await SpotifySdk.connectToSpotifyRemote(
clientId: clientID,
redirectUrl: redirectURI,
accessToken: token,
scope: scope,
);
} I also tested that my custom token handler works with: {
// get access token from server
final res = await http.post(...);
final token = jsonDecode(res.body)['access_token'] as String;
SpotifySdkPlugin.getOAuthToken = () async {
print('getOAuthToken custom cb $token');
return token;
};
await SpotifySdk.connectToSpotifyRemote(
clientId: clientID,
redirectUrl: redirectURI,
playerName: 'JukeLab',
accessToken: token,
scope: scope,
); Due to having a server side access / refresh token, my app loads the player with no auth prompt! |
In general looks good. Let's also update the readme to indicate this new feature, how developers can use it and clearly state that it is "web only". |
@@ -735,6 +745,16 @@ external set _onSpotifyWebPlaybackSDKReady(void Function()? f); | |||
@JS('onSpotifyWebPlaybackSDKReady') | |||
external void Function()? get _onSpotifyWebPlaybackSDKReady; | |||
|
|||
/// Allows assigning the function getOAuthToken |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Allows assigning the function getOAuthToken | |
/// Allows getting the function getOAuthToken |
Since this is for the getter shouldn't that be getting
?
@nzoschke something that came to mind, if in an alternative implementation, you only passed a "refresh token" endpoint instead of a function would that be enough for you? I am thinking that on the iOS SDK there is an alternative authentication flow that works like this. Also other generic oAuth libraries that I have used work in similar way. If you set the refresh endpoint then they use it. We could potentially align the APIs. My counter suggestion would look like this:
|
Yes I think that would work. My server side components were built with this token swap pattern in mind. Support for this in the iOS SDK would be great, and I see how this could work with the web SDK for my use case too. Here's another reference doc about the token swap: https://developer.spotify.com/documentation/ios/guides/token-swap-and-refresh/ |
I think I understand it a bit better now. The iOS SDK does a bit more magic
and calls your refresh endpoint automatically. However on the web SDK you
need to write that code yourself and pass it as a function.
Can you post an example of a real life token refresh function? Could it be
worth including that in this library? In that case the end developer would
only need to pass the refresh URL and our library would make the call.
…On Tue 29. Jun 2021 at 13:40, Noah Zoschke ***@***.***> wrote:
Yes I think that would work.
My server side components were built with this token swap pattern in mind.
Support for this in the iOS SDK would be great, and I see how this could
work with the web SDK for my use case too.
Here's another reference doc about the token swap:
https://developer.spotify.com/documentation/ios/guides/token-swap-and-refresh/
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#116 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AARX7D35FTPX7DZGKWEE36LTVGPJPANCNFSM47IEXI6A>
.
|
Here is a fairly complete example. This is using:
As I do intend to support iOS for my app, I did end up following the token swap pattern as documented in:
I think this pattern makes sense for both iOS and web. Future permissionCheck() async {
var apiURI = 'http://localhost:8000';
var clientID = '<REDACTED>';
var redirectURI = 'http://localhost:8686/callback.html';
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(',');
final url = Uri.https('accounts.spotify.com', '/authorize', {
'client_id': clientID,
'redirect_uri': redirectURI,
'response_type': 'code',
'scope': scope,
});
final result = await FlutterWebAuth.authenticate(
url: url.toString(),
callbackUrlScheme: redirectURI,
);
// swap code for token
final res = await http.post(
Uri.parse('$apiURI/api/v1/spotify/token'),
body: {
'code': Uri.parse(result).queryParameters['code'],
'redirect_uri': redirectURI,
},
);
var out = jsonDecode(res.body);
var now = (DateTime.now().millisecondsSinceEpoch / 1000).round();
var token = SpotifyToken(
accessToken: out['access_token'] as String,
clientId: clientID,
expiry: now + out['expires_in'] as int,
refreshToken: out['refresh_token'] as String,
);
SpotifySdkPlugin.getOAuthToken = () async {
// refresh token if expired
if (token.expiry <= DateTime.now().millisecondsSinceEpoch / 1000) {
print('getOAuthToken refresh ${token.expiry}');
var rt = token.refreshToken;
final res = await http.post(
Uri.parse('$apiURI/api/v1/spotify/refresh'),
body: {
'refresh_token': rt,
},
);
var out = jsonDecode(res.body);
var now = (DateTime.now().millisecondsSinceEpoch / 1000).round();
token = SpotifyToken(
accessToken: out['access_token'] as String,
clientId: clientID,
expiry: now + out['expires_in'] as int,
refreshToken: rt,
);
}
print('getOAuthToken ${token.accessToken}');
return token.accessToken;
};
var ok = await SpotifySdk.connectToSpotifyRemote(
clientId: clientID,
redirectUrl: redirectURI,
accessToken: token.accessToken,
scope: scope,
);
print('connect $ok');
} |
So I too could see a design that uses the token swap settings for the web SDK. We introduce these:
So if the token URLs are not set, it does the existing Authorization Code with PKCE auth flow and refresh. If these are set, it does an Authorization Code (without PKCE) to get a code, then follows the well defined token swap spec (docs) to swap the code for an access token and to refresh the token. The pros are:
The cons are:
So @fotiDim given all of the above do you have a preference between (1) this existing PR for |
Here's a first pass at what doing the token swap stuff in the web SDK might look like. I think it's feeling good to bake this in, certainly simpler than all the stuff I had to figure out on my own to do the flow and pass the refresh function down. One thing is I'll want to get the access token out to use with the web API client. diff --git a/lib/spotify_sdk_web.dart b/lib/spotify_sdk_web.dart
index bd0475d..9654661 100644
--- a/lib/spotify_sdk_web.dart
+++ b/lib/spotify_sdk_web.dart
@@ -83,6 +83,17 @@ class SpotifySdkPlugin {
static const String DEFAULT_SCOPES =
'streaming user-read-email user-read-private';
+ static String? _tokenSwapURL;
+ static String? _tokenRefreshURL;
+
+ static set tokenSwapURL(String s) {
+ _tokenSwapURL = s;
+ }
+
+ static set tokenRefreshURL(String s) {
+ _tokenRefreshURL = s;
+ }
+
/// constructor
SpotifySdkPlugin(
this.playerContextEventController,
@@ -403,19 +414,36 @@ class SpotifySdkPlugin {
}
/// Authenticates a new user with Spotify and stores access token.
- Future<String> _authorizeSpotify(
- {required String clientId,
- required String redirectUrl,
- required String? scopes}) async {
+ Future<String> _authorizeSpotify({
+ required String clientId,
+ required String redirectUrl,
+ required String? scopes,
+ }) async {
+ print('_authorizeSpotify $_tokenSwapURL');
// creating auth uri
var codeVerifier = _createCodeVerifier();
var codeChallenge = _createCodeChallenge(codeVerifier);
var state = _createAuthState();
- var authorizationUri =
- 'https://accounts.spotify.com/authorize?client_id=$clientId&response_type=code&redirect_uri=$redirectUrl&code_challenge_method=S256&code_challenge=$codeChallenge&state=$state&scope=$scopes';
+
+ var params = {
+ 'client_id': clientId,
+ 'redirect_uri': redirectUrl,
+ 'response_type': 'code',
+ 'state': state,
+ 'scope': scopes,
+ };
+
+ if (_tokenSwapURL == null) {
+ params['code_challenge_method'] = 'S256';
+ params['code_challenge'] = codeChallenge;
+ }
+
+ final authorizationUri =
+ Uri.https('accounts.spotify.com', 'authorize', params);
// opening auth window
- var authPopup = window.open(authorizationUri, 'Spotify Authorization');
+ var authPopup =
+ window.open(authorizationUri.toString(), 'Spotify Authorization');
String? message;
var sub = window.onMessage.listen(allowInterop((event) {
message = event.data.toString();
@@ -461,20 +489,38 @@ class SpotifySdkPlugin {
}
await sub.cancel();
- // exchange auth code for access and refresh tokens
dynamic authResponse;
+
+ // build request to exchange auth code with PKCE for access and refresh tokens
+ var req = RequestOptions(
+ path: 'https://accounts.spotify.com/api/token',
+ method: 'POST',
+ data: {
+ 'client_id': clientId,
+ 'grant_type': 'authorization_code',
+ 'code': parsedMessage.queryParameters['code'],
+ 'redirect_uri': redirectUrl,
+ 'code_verifier': codeVerifier
+ },
+ contentType: Headers.formUrlEncodedContentType,
+ );
+
+ // or build request to exchange code with token swap
+ // https://developer.spotify.com/documentation/ios/guides/token-swap-and-refresh/
+ if (_tokenSwapURL != null) {
+ req = RequestOptions(
+ path: _tokenSwapURL!,
+ method: 'POST',
+ data: {
+ 'code': parsedMessage.queryParameters['code'],
+ 'redirect_uri': redirectUrl,
+ },
+ contentType: Headers.formUrlEncodedContentType,
+ );
+ }
+
try {
- authResponse = (await _authDio.post(
- 'https://accounts.spotify.com/api/token',
- data: {
- 'client_id': clientId,
- 'grant_type': 'authorization_code',
- 'code': parsedMessage.queryParameters['code'],
- 'redirect_uri': redirectUrl,
- 'code_verifier': codeVerifier
- },
- options: Options(contentType: Headers.formUrlEncodedContentType)))
- .data;
+ authResponse = (await _authDio.fetch(req)).data;
} on DioError catch (e) {
print('Spotify auth error: ${e.response?.data}');
rethrow;
@@ -492,15 +538,36 @@ class SpotifySdkPlugin {
/// Refreshes the Spotify access token using the refresh token.
Future<dynamic> _refreshSpotifyToken(
String? clientId, String? refreshToken) async {
+ print('refresh?');
+ // build request to refresh PKCE for access and refresh tokens
+ var req = RequestOptions(
+ path: 'https://accounts.spotify.com/api/token',
+ method: 'POST',
+ data: {
+ 'grant_type': 'refresh_token',
+ 'refresh_token': refreshToken,
+ 'client_id': clientId,
+ },
+ contentType: Headers.formUrlEncodedContentType,
+ );
+
+ // or build request to refresh code with token swap
+ // https://developer.spotify.com/documentation/ios/guides/token-swap-and-refresh/
+ if (_tokenRefreshURL != null) {
+ req = RequestOptions(
+ path: _tokenRefreshURL!,
+ method: 'POST',
+ data: {
+ 'refresh_token': refreshToken,
+ },
+ contentType: Headers.formUrlEncodedContentType,
+ );
+ }
+
try {
- return (await _authDio.post('https://accounts.spotify.com/api/token',
- data: {
- 'grant_type': 'refresh_token',
- 'refresh_token': refreshToken,
- 'client_id': clientId,
- },
- options: Options(contentType: Headers.formUrlEncodedContentType)))
- .data;
+ var d = (await _authDio.fetch(req)).data;
+ d['refresh_token'] = refreshToken;
+ return d;
} on DioError catch (e) {
print('Token refresh error: ${e.response?.data}');
rethrow; |
That is great I was about to suggest that. My current thinking is:
Given the above my vote goes for adopting @brim-borium since this is a strategic decision that we need to make, do you have an opinion on this? |
See #121 for a PR implementing the token swap pattern for web |
Shall we close this in favour of #121? |
Yes closing in favor of #121. |
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 pass in a custom
getOAuthToken
function which allows them to customize the auth and refresh flow.The use case is for Spotify apps that use the standard Authorization Code flow and manage access and refresh tokens with a server side component.
Fixes #112
TODO: