adamint / spotify-web-api-kotlin

Spotify Web API wrapper for Kotlin, Java, JS, and Native - Targets JVM, Android, JS (browser), Native (Desktop), and Apple tvOS/iOS. Includes a Spotify Web Playback SDK wrapper for Kotlin/JS, and a spotify-auth wrapper for Kotlin/Android.
https://adamint.github.io/spotify-web-api-kotlin-docs/
MIT License
194 stars 21 forks source link

Android invalid grant, code verifier was incorrect #306

Closed rsicarelli closed 2 years ago

rsicarelli commented 2 years ago

Description

I'm trying to use the PKCE flow on Android and I'm getting the following error:

responseCode=400, body={"error":"invalid_grant","error_description":"code_verifier was incorrect"}

Apparently, the reason is that the AbstractSpotifyPkceLoginActivity:pkceCodeVerifier attribute gets recreated after redirecting from the opened URL from the device browser.

To reproduce

  1. Open the target app
  2. Click in a "Login" button to launch the Activity extending the AbstractSpotifyPkceLoginActivity
  3. On creation the pkceCodeVerifier is Gxo3to4KqcxPSN9udvacLxrq3xWmyrohY5yIMhHrFhqhMKmopFefepux9nZ1M2cXwbCUHaDnndSb2ZUtwq4POGmADh2RMlyeu
  4. User is redirected to the browser to accept the app
  5. User accepts the permission and gets redirected to the app
  6. When user is redirected back to the app, a new instance of the class extending AbstractSpotifyPkceLoginActivity is created. Now, the pkceCodeVerifier code is eBId4eXjoWrRrY6xdJCTDHe9ETcorXqLEWU4rKbdyTIqzDUNNskTHcS5awd2dH4UPDcyWPHodnbi1teNdo8U68dYDsNqBDGRv
  7. Function handleSpotifyAuthenticationResponse is called with the response. No errors
  8. When trying to create a new spotify api client, the pkceCodeVerifier is different than the original request on step 3, now using the one created on step 6
  9. Error on this line is thrown. Full stacktrace:
Click to expand ``` com.adamratzman.spotify.SpotifyException$AuthenticationException: Invalid credentials provided in the login process (clientId=..., clientSecret=null, authCode=....) 2022-03-14 15:53:50.165 W/System.err: at com.adamratzman.spotify.SpotifyClientApiBuilder.build(SpotifyApiBuilder.kt:962) 2022-03-14 15:53:50.165 W/System.err: at com.adamratzman.spotify.SpotifyClientApiBuilder$build$1.invokeSuspend(Unknown Source:15) 2022-03-14 15:53:50.165 W/System.err: at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) 2022-03-14 15:53:50.165 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(SuspendFunctionGun.kt:191) 2022-03-14 15:53:50.165 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:147) 2022-03-14 15:53:50.166 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(SuspendFunctionGun.kt:15) 2022-03-14 15:53:50.166 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(SuspendFunctionGun.kt:93) 2022-03-14 15:53:50.166 W/System.err: at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46) 2022-03-14 15:53:50.166 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(SuspendFunctionGun.kt:191) 2022-03-14 15:53:50.166 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:147) 2022-03-14 15:53:50.166 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(SuspendFunctionGun.kt:15) 2022-03-14 15:53:50.166 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(SuspendFunctionGun.kt:93) 2022-03-14 15:53:50.166 W/System.err: at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46) 2022-03-14 15:53:50.166 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(SuspendFunctionGun.kt:191) 2022-03-14 15:53:50.166 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:147) 2022-03-14 15:53:50.166 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(SuspendFunctionGun.kt:15) 2022-03-14 15:53:50.166 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(SuspendFunctionGun.kt:93) 2022-03-14 15:53:50.166 W/System.err: at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46) 2022-03-14 15:53:50.166 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(SuspendFunctionGun.kt:191) 2022-03-14 15:53:50.166 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:147) 2022-03-14 15:53:50.166 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(SuspendFunctionGun.kt:15) 2022-03-14 15:53:50.166 W/System.err: at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(SuspendFunctionGun.kt:93) 2022-03-14 15:53:50.166 W/System.err: at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46) 2022-03-14 15:53:50.166 W/System.err: at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106) 2022-03-14 15:53:50.166 W/System.err: at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279) 2022-03-14 15:53:50.166 W/System.err: at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85) 2022-03-14 15:53:50.166 W/System.err: at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59) 2022-03-14 15:53:50.166 W/System.err: at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1) 2022-03-14 15:53:50.166 W/System.err: at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38) 2022-03-14 15:53:50.166 W/System.err: at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source:1) 2022-03-14 15:53:50.166 W/System.err: at com.adamratzman.spotify.auth.pkce.AbstractSpotifyPkceLoginActivity.onResume(AbstractSpotifyPkceLoginActivity.kt:106) 2022-03-14 15:53:50.166 W/System.err: at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1571) 2022-03-14 15:53:50.166 W/System.err: at android.app.Activity.performResume(Activity.java:8141) 2022-03-14 15:53:50.166 W/System.err: at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4626) 2022-03-14 15:53:50.166 W/System.err: at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4668) 2022-03-14 15:53:50.166 W/System.err: at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52) 2022-03-14 15:53:50.166 W/System.err: at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176) 2022-03-14 15:53:50.166 W/System.err: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97) 2022-03-14 15:53:50.166 W/System.err: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2251) 2022-03-14 15:53:50.166 W/System.err: at android.os.Handler.dispatchMessage(Handler.java:106) 2022-03-14 15:53:50.166 W/System.err: at android.os.Looper.loop(Looper.java:233) 2022-03-14 15:53:50.166 W/System.err: at android.app.ActivityThread.main(ActivityThread.java:8068) 2022-03-14 15:53:50.166 W/System.err: at java.lang.reflect.Method.invoke(Native Method) 2022-03-14 15:53:50.166 W/System.err: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:631) 2022-03-14 15:53:50.166 W/System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:978) 2022-03-14 15:53:50.167 W/System.err: Caused by: com.adamratzman.spotify.SpotifyException$ParseException: Unable to parse {"error":"invalid_grant","error_description":"code_verifier was incorrect"} (Fields [access_token, token_type, expires_in] are required for type with serial name 'com.adamratzman.spotify.models.Token', but they were missing) 2022-03-14 15:53:50.167 W/System.err: at com.adamratzman.spotify.models.serialization.SerializationUtilsKt.parseJson(SerializationUtils.kt:33) 2022-03-14 15:53:50.167 W/System.err: at com.adamratzman.spotify.models.serialization.SerializationUtilsKt.toObject(SerializationUtils.kt:41) 2022-03-14 15:53:50.167 W/System.err: at com.adamratzman.spotify.SpotifyClientApiBuilder.build(SpotifyApiBuilder.kt:952) 2022-03-14 15:53:50.167 W/System.err: ... 44 more 2022-03-14 15:53:50.167 W/System.err: Caused by: kotlinx.serialization.MissingFieldException: Fields [access_token, token_type, expires_in] are required for type with serial name 'com.adamratzman.spotify.models.Token', but they were missing 2022-03-14 15:53:50.167 W/System.err: at kotlinx.serialization.internal.PluginExceptionsKt.throwMissingFieldException(PluginExceptions.kt:20) 2022-03-14 15:53:50.167 W/System.err: at com.adamratzman.spotify.models.Token.(Authentication.kt:25) 2022-03-14 15:53:50.167 W/System.err: at com.adamratzman.spotify.models.Token$$serializer.deserialize(Authentication.kt:25) 2022-03-14 15:53:50.167 W/System.err: at com.adamratzman.spotify.models.Token$$serializer.deserialize(Authentication.kt:25) 2022-03-14 15:53:50.167 W/System.err: at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:59) 2022-03-14 15:53:50.167 W/System.err: at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:36) 2022-03-14 15:53:50.167 W/System.err: at kotlinx.serialization.json.Json.decodeFromString(Json.kt:100) 2022-03-14 15:53:50.167 W/System.err: at com.adamratzman.spotify.models.serialization.SerializationUtilsKt$toObject$1.invoke(SerializationUtils.kt:42) 2022-03-14 15:53:50.167 W/System.err: at com.adamratzman.spotify.models.serialization.SerializationUtilsKt$toObject$1.invoke(SerializationUtils.kt:41) 2022-03-14 15:53:50.167 W/System.err: at com.adamratzman.spotify.models.serialization.SerializationUtilsKt.parseJson(SerializationUtils.kt:31) 2022-03-14 15:53:50.167 W/System.err: ... 46 more ```

Workaround

Overriding the variable pkceCodeVerifier on my Activity, setting to a fixed string works as expected:

class SpotifyPkceLoginActivityImpl : AbstractSpotifyPkceLoginActivity() {
    ...
    override val pkceCodeVerifier: String
        get() = "testingaoirao9reiw9riw4e9ifdksa9fjsk9ujw3495iuwe9ids"

    override fun onSuccess(api: SpotifyClientApi) {
        //Success is called with the api
        setResult(Activity.RESULT_OK)
    }

    override fun onFailure(exception: Exception) {
        setResult(Activity.RESULT_CANCELED)
    }
}

Expected behavior For what I understood, the pkceCodeVerifier attribute must be preserved/saved after user gets back to the app.

Project configuration

//build.gradle.kts
implementation("com.adamratzman:spotify-api-kotlin-android:3.8.5")
<application>
    <activity
        android:name=".presentation.SpotifyPkceLoginActivityImpl"
        android:exported="true"
        android:launchMode="singleTop"
        android:theme="@style/Theme.AppCompat.Light">
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />

            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />

            <data
                android:host="..." //redacted
                android:scheme="..." //redacted />
        </intent-filter>
    </activity>
</application>

Smartphone:

Additional info

adamint commented 2 years ago

Thank you for the detailed bug report. Instead of a new code verifier being created on activity creation, it should live in the credential provider and be reset when the activity is to be started. @rsicarelli would you be able to confirm this cannot be reproduced after the next minor version releases tomorrow? I will comment here then.

rsicarelli commented 2 years ago

@adamint sure thing, let me know once its available!

adamint commented 2 years ago

@rsicarelli 3.8.6 is available (syncing)

rsicarelli commented 2 years ago

@adamint good job, the fix works! Many thanks for your support

adamint commented 2 years ago

Wonderful, glad to hear it. I’ll close the issue now