Repository: ably/ably-flutter
Issue: Token Authentication with authCallback in Android: `java.lang.Exception: Invalid authCallback response`

When using token authentication by setting the authCallback in ably-flutter running on Android devices, an exception is always thrown. Token authentication still works because this method is called until this method does return the value.

More detail: The ably-java expects a synchronous return value when it calls `Object authCallbackResponse = tokenOptions.authCallback.getTokenRequest(params);` to call the `authCallback` the user has set. In normal Android this returns a value immediately, but in Ably-flutter this is actually an asynchronous request to the dart side so it doesn’t return anything (null). Therefore, in `Auth.java` in ably-java, `authCallbackResponse` is null, and `throw AblyException.fromErrorInfo(new ErrorInfo("Invalid authCallback response", 400, 40000));` is called. 

https://github.com/ably/ably-flutter/blob/36b604fdaed87342edc2fd0c9ad94c34d362148d/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java#L425-L452

The relevant ably-java [code](https://github.com/ably/ably-java/blob/e73a3bdb205f188baea891003cbc1af058e0db73/lib/src/main/java/io/ably/lib/rest/Auth.java#L579-L589) is:

```java
            try {
                /* the callback can return either a signed token request, or a TokenDetails */
                Object authCallbackResponse = tokenOptions.authCallback.getTokenRequest(params);
                if(authCallbackResponse instanceof String)
                    return new TokenDetails((String)authCallbackResponse);
                if(authCallbackResponse instanceof TokenDetails)
                    return (TokenDetails)authCallbackResponse;
                if(authCallbackResponse instanceof TokenRequest)
                    signedTokenRequest = (TokenRequest)authCallbackResponse;
                else
                    throw AblyException.fromErrorInfo(new ErrorInfo("Invalid authCallback response", 400, 40000)); // <-- This gets called because `authCallbackResponse` is null
```

Token authentication still works, but unfortunately the exception is not nice to see/ stack trace is scary.

I first came across this error in May 2021, but misdiagnosed it to be about "not handling clientIds properly". So I will close https://github.com/ably/ably-flutter/issues/122

To reproduce this, I have a demo application which uses token auth: https://github.com/ben-xD/ably-flutter-token or you can just try to use token auth with your own token auth server.



┆Issue is synchronized with this [Jira Bug](https://ably.atlassian.net/browse/SDK-1154) by [Unito](https://www.unito.io)


Changed files:
android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java

diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java b/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java
index 08a49ff0..ae68c4dc 100644
--- a/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java
+++ b/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java
@@ -11,6 +11,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.CountDownLatch;
 
 import io.ably.flutter.plugin.generated.PlatformConstants;
 import io.ably.flutter.plugin.push.PushActivationEventHandlers;
@@ -183,18 +184,24 @@ public class AblyMethodCallHandler implements MethodChannel.MethodCallHandler {
           clientOptions.options.authCallback = (Auth.TokenParams params) -> {
             Object token = ablyLibrary.getRestToken(handle);
             if (token != null) return token;
+
+            final CountDownLatch latch = new CountDownLatch(1);
             new Handler(Looper.getMainLooper()).post(() -> {
               AblyFlutterMessage<Auth.TokenParams> channelMessage = new AblyFlutterMessage<>(params, handle);
               channel.invokeMethod(PlatformConstants.PlatformMethod.authCallback, channelMessage, new MethodChannel.Result() {
                 @Override
                 public void success(@Nullable Object result) {
                   ablyLibrary.setRestToken(handle, result);
+                  latch.countDown();
                 }
 
                 @Override
                 public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {
                   System.out.println(errorDetails);
-                  //Do nothing, let another request go to flutter side!
+                  if (errorMessage != null) {
+                    result.error("40000", String.format("Error from authCallback: %s", errorMessage), errorDetails);
+                  }
+                  latch.countDown();
                 }
 
                 @Override
@@ -204,7 +211,13 @@ public class AblyMethodCallHandler implements MethodChannel.MethodCallHandler {
               });
             });
 
-            return null;
+            try {
+              latch.await();
+            } catch (InterruptedException e) {
+              throw AblyException.fromErrorInfo(e, new ErrorInfo("Exception while waiting for authCallback to return", 400, 40000));
+            }
+
+            return ablyLibrary.getRestToken(handle);
           };
         }
         result.success(ablyLibrary.createRest(clientOptions.options));
@@ -431,7 +444,7 @@ public class AblyMethodCallHandler implements MethodChannel.MethodCallHandler {
   }
 
   private void createRealtimeWithOptions(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
-    final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments;
+    final AblyFlutterMessage<PlatformClientOptions> message = (AblyFlutterMessage<PlatformClientOptions>) call.arguments;
     this.<PlatformClientOptions>ablyDo(message, (ablyLibrary, clientOptions) -> {
       try {
         final long handle = ablyLibrary.getCurrentHandle();
@@ -439,20 +452,26 @@ public class AblyMethodCallHandler implements MethodChannel.MethodCallHandler {
           clientOptions.options.authCallback = (Auth.TokenParams params) -> {
             Object token = ablyLibrary.getRealtimeToken(handle);
             if (token != null) return token;
+
+            final CountDownLatch latch = new CountDownLatch(1);
             new Handler(Looper.getMainLooper()).post(() -> {
-              AblyFlutterMessage channelMessage = new AblyFlutterMessage<>(params, handle);
+              AblyFlutterMessage<Auth.TokenParams> channelMessage = new AblyFlutterMessage<>(params, handle);
               channel.invokeMethod(PlatformConstants.PlatformMethod.realtimeAuthCallback, channelMessage, new MethodChannel.Result() {
                 @Override
                 public void success(@Nullable Object result) {
                   if (result != null) {
                     ablyLibrary.setRealtimeToken(handle, result);
+                    latch.countDown();
                   }
                 }
 
                 @Override
                 public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {
                   System.out.println(errorDetails);
-                  //Do nothing, let another request go to flutter side!
+                  if (errorMessage != null) {
+                    result.error("40000", String.format("Error from authCallback: %s", errorMessage), errorDetails);
+                  }
+                  latch.countDown();
                 }
 
                 @Override
@@ -461,7 +480,14 @@ public class AblyMethodCallHandler implements MethodChannel.MethodCallHandler {
                 }
               });
             });
-            return null;
+
+            try {
+              latch.await();
+            } catch (InterruptedException e) {
+              throw AblyException.fromErrorInfo(e, new ErrorInfo("Exception while waiting for authCallback to return", 400, 40000));
+            }
+
+            return ablyLibrary.getRealtimeToken(handle);
           };
         }
         result.success(ablyLibrary.createRealtime(clientOptions.options));