rinukkusu / spotify-dart

A dart library for interfacing with the Spotify API.
BSD 3-Clause "New" or "Revised" License
191 stars 91 forks source link

Expose `codeVerifier` when building `AuthorizationCodeGrant` #201

Closed gmpassos closed 4 months ago

gmpassos commented 5 months ago

The library needs to expose the parameter codeVerifier so that we can use a personalized or database-stored codeVerifier.

This needs to be done in SpotifyApi.authorizationCodeGrant and SpotifyApiBase.authorizationCodeGrant, and then passed to oauth2.AuthorizationCodeGrant.

hayribakici commented 4 months ago

@gmpassos Could you maybe give an example on how this could look like? Better, open a PR? :)

gmpassos commented 4 months ago

What is necessary is be able to call SpotifyApi.authorizationCodeGrant passing an extra parameter codeVerifier (optional).


SpotifyApi.authorizationCodeGrant(credentials, codeVerifier: codeVerifier);

Then internally it needs to repass it to SpotifyApiBase.authorizationCodeGrant:

SpotifyApiBase.authorizationCodeGrant(credentials, http.Client(), onCredentialsRefreshed, codeVerifier: codeVerifier);

And then oauth2.AuthorizationCodeGrant should be called passing codeVerifier.

Here's a real world OAuth integration with Spotify in Dart using a DB stored codeVerifier:

(relevant code only):

import 'dart:math';
import 'dart:convert';

import 'package:spotify/spotify.dart';
import 'package:oauth2/oauth2.dart' as oauth2;
import 'package:http/http.dart' as http;

//...

Future<String?> getAuthorizationUrl(int establishmentId) async {
  //...

  final clientId = _clientId!;
  final clientSecret = _clientSecret!;
  final redirectUrl = _redirectUri!;

  final credentials = SpotifyApiCredentials(clientId, clientSecret);

  final info = <String, dynamic>{
    'establishmentId': establishmentId,
  };

  final state = json.encode(info);
  final codeVerifier = _createCodeVerifier(); // my own code verifier.

  // save in the DB the OAuth integration data:
  var oAuthIntegration = await _saveOAuthIntegration(
    establishmentId,
    clientId,
    clientSecret,
    codeVerifier: codeVerifier,
  );

  if (oAuthIntegration == null) {
    return throw StateError("Invalid `oAuthIntegration`");
  }

  final grant = oauth2.AuthorizationCodeGrant(
    credentials.clientId!,
    Uri.parse(_spotifyAuthorizationUrl),
    Uri.parse(_spotifyTokenUrl),
    secret: credentials.clientSecret,
    httpClient: http.Client(),
    codeVerifier: codeVerifier,
  );

  var redirectUri = Uri.parse(redirectUrl);

  var authorizationUrl = grant.getAuthorizationUrl(
    redirectUri,
    scopes: _spotifyScopes,
    state: state,
  );

  return authorizationUrl.toString();
}

Future<bool> oauth(
    String? code, String? state, String requestedUri) async {
  if (code == null || code.isEmpty) {
    throw StateError("Invalid `code`");
  }

  if (state == null || state.isEmpty) {
    throw StateError("Invalid `state`");
  }

  var parameters = json.decode(state) as Map;

  var establishmentId = parameters['establishmentId'] as int?;
  if (establishmentId == null) {
    throw StateError("Invalid `establishmentId`");
  }

  //...

  final clientId = _clientId!;
  final clientSecret = _clientSecret!;
  final redirectUrl = _redirectUri!;

  var oAuthIntegration = await _getOAuthIntegration(
      establishmentId, clientId, clientSecret);

  if (oAuthIntegration == null) {
    throw StateError("Invalid `oAuthIntegration`");
  }

  final codeVerifier = oAuthIntegration.codeVerifier;
  if (codeVerifier == null || codeVerifier.isEmpty) {
    throw StateError("Invalid `codeVerifier`");
  }

  final credentials = SpotifyApiCredentials(clientId, clientSecret);

  //
  // Can't pass `codeVerifier` to:
  //   SpotifyApi.authorizationCodeGrant
  //
  // Using `oauth2.AuthorizationCodeGrant` directly:
  final grant = oauth2.AuthorizationCodeGrant(
    credentials.clientId!,
    Uri.parse(_spotifyAuthorizationUrl),
    Uri.parse(_spotifyTokenUrl),
    secret: credentials.clientSecret,
    httpClient: http.Client(),
    codeVerifier: codeVerifier,
  );

  var redirectUri = Uri.parse(redirectUrl);

  var authorizationUrl = grant.getAuthorizationUrl(
    redirectUri,
    scopes: _spotifyScopes,
    state: state,
  );

  print('-- authorizationUrl: $authorizationUrl');

  final spotify = SpotifyApi.fromAuthCodeGrant(grant, requestedUri);

  var resolvedCredential = await spotify.getCredentials();

  var oAuthIntegration2 = await oAuthIntegrationModule
      .save(
        oAuthIntegration.id!,
        codeVerifier: '',
        accessToken: resolvedCredential.accessToken,
        refreshToken: resolvedCredential.refreshToken,
        scopes: resolvedCredential.scopes,
        expires: resolvedCredential.expiration,
        refreshTokenURL: resolvedCredential.tokenEndpoint.toString(),
      )
      .payloadAsync;

  if (oAuthIntegration2 == null) {
    throw StateError("Invalid `oAuthIntegration2`");
  }

  return true;
}
gmpassos commented 4 months ago

PR:

https://github.com/rinukkusu/spotify-dart/pull/205

rinukkusu commented 4 months ago

Thank you for your contribution - it's published with version 0.13.2!