Search code examples
javaphplaravelpusherlaravel-passport

Problem with authenticating private channels in laravel with java client


I want to send broadcast messages from server (using laravel) to clients (using java).

What I'm using

  1. Pusher as boradcast driver.
  2. laravel passport for api authentication.

What I've done in server side

  1. I've configured my Pusher credentials in .env file.
  2. Uncommented App\Providers\BroadcastServiceProvider::class line in config/app.php file.
  3. In config/auth.php file I've added the following:
'guards' => [
     'web' => [
         'driver' => 'session',
         'provider' => 'users',
     ],

     'devices' => [
         'driver' => 'session',
         'provider' => 'devices',
     ],

     'api' => [
         'driver' => 'passport',
         'provider' => 'devices',
     ],
 ],

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\User::class,
    ],

     // using devices table to authenticate over api guard
    'devices' => [
        'driver' => 'eloquent',
        'model' => App\Device::class,
    ],
],
  1. In App\Providers\BroadcastServiceProvider class I added the following to boot() function:
Broadcast::routes(['prefix' => 'api', 'middleware' => 'auth:api']);
  1. In routes/channels.php I added the following:
Broadcast::channel('device.{device_id}', function ($device, $device_id) {
    return $device->id === $device_id;
});
  1. Created an event AdvertisementAdded by running php artisan make:event AdvertisementAdded, added implements ShouldBroadcast then added the following to its broadcastOn() method:
return new PrivateChannel('device.'.$this->device_id);

What I've done in client side

Because I'm just testing now, I got my access_token and device_id by sending a login request from postman

Getting access_token from postman

I copied that accessToken to my java client and stored it in accessToken variable as String, here's the code:

String accessToken = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImY3ZTVlMTAzZWE3MzJjMTI5NzY1YTliMmMzOTM0N2ZhOGE4OTU5MjRjNDA5ZjgyOTA4ZDg5NTFjZTBkOGZlNTA2M2M1YTI1MDBlOTdhZDdiIn0.eyJhdWQiOiIxIiwianRpIjoiZjdlNWUxMDNlYTczMmMxMjk3NjVhOWIyYzM5MzQ3ZmE4YTg5NTkyNGM0MDlmODI5MDhkODk1MWNlMGQ4ZmU1MDYzYzVhMjUwMGU5N2FkN2IiLCJpYXQiOjE1NTkwOTYyNDgsIm5iZiI6MTU1OTA5NjI0OCwiZXhwIjoxNTkwNzE4NjQ3LCJzdWIiOiI3Iiwic2NvcGVzIjpbXX0.FKeE9Z-wv2yUNQPl-qsbu9baYGTdbQ6DuzaI1R8azR6l1CIP9uRI4hCaoWvgx0GXWWLPRNhfQl-YD3KP2YOraW16-h4ie_95B9VQrpFxXnlqKojsfh1xSrSNSl5HncslMWQPVjoesBpM5y_cpG19PGgu-SWo0W6s9Fiz_Nm70oyyZB9mSqU8PVQvAOSNr6TMR0aC3iMLFfkyZkTSwj8EoRyD2LGW6v4PFriqx8JLbZASCOiUYBlYnunWrTFDOAenZcoa5Sw7u7kbSvYehjDKRwKjQM6zmPfi0A3Mp0CHjHE599OXb-NG2IMH-wmlT0vEZjP2U97hxmsNW1RtHNXWaRKFL9T-WVmZbJf3fH5hXqTv495L3MQfq_m5YFHyc5NuIqK4K4xMJB956a33ICnH8DmvPmJgderNAhqEX1JHUAsR63K7xbZxRBDS8OlQYcEf-_v75X0kT1s067enSvI8Vs212AVnI6k0FmgQNM8DfJUq6YduD0m2F2ZWpKPrwdd6PdW5ZlZTEv-D8dYIEQ_CwOWohNoENATmTqxDpPBxK5c723MEt8S7Sa9MEGAo56HW3-9pbazbEdY1GqPWKVkov7K_6eBFcWsV67AgJpoKFt6RiBfRvokgiH96WG89qBB_Ucpm8uBahX93FaOXhVLW0VjJH2LQKrGw0bb5LS8Ql5o";
String deviceId = "7";

Map<String, String> authHeaders = new HashMap();

authHeaders.put("Authorization", accessToken);

HttpAuthorizer authorizer = new HttpAuthorizer("http://localhost:8000/api/broadcasting/auth");
authorizer.setHeaders(authHeaders);
PusherOptions options = new PusherOptions();
options.setAuthorizer(authorizer).setCluster(PUSHER_CLUSTER);
Pusher pusher = new Pusher(PUSHER_APP_KEY, options);

pusher.subscribePrivate("private-device." + deviceId, new PrivateChannelEventListener() {
    @Override
    public void onEvent(String channelName, String eventName, final String data) {
        System.out.println(String.format("Received event on channel [%s]", channelName));
    }

    @Override
    public void onSubscriptionSucceeded(String string) {
        System.out.println(String.format("Subscribed to channel [%s]", string));
    }

    @Override
    public void onAuthenticationFailure(String string, Exception excptn) {
        System.out.println(string);
    }
});

pusher.connect(new ConnectionEventListener() {
    @Override
    public void onConnectionStateChange(ConnectionStateChange change) {
        System.out.println("State changed to " + change.getCurrentState() +
                           " from " + change.getPreviousState());
    }

    @Override
    public void onError(String message, String code, Exception e) {
        System.out.println("There was a problem connecting!");
    }
});

// Keeping main thread alive
while (true) {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

When running the code above, it outputs the following on console:

State changed to CONNECTING from DISCONNECTED
State changed to CONNECTED from CONNECTING
java.io.IOException: Server returned HTTP response code: 403 for URL: http://localhost:8000/api/broadcasting/auth

I'm sure that auth:api middleware is working as I expect on other requests.

Here's a snippet from my routes/api.php:

Route::middleware('auth:api')->group(function () {
    Route::prefix('advertisements')->group(function () {
        Route::get('/request', 'AdvertisementsController@getDeviceAdvertisements')
            ->name('advertisements.getDeviceAdvertisements');
    });
});

And here's a test to that route from postman (with the same access token as above):

Successful api authentication test on postman

And here's a test to api/broadcasting/auth route from postman (with the same access token as above):

Failed api authentication on api/broadcasting/auth route

What's the problem? Why all api routes under auth:api middleware working properly but not api/broadcasting/auth route??

Note

I tried working with public channels with no problems.


Solution

  • After a whole day of searching, finally It's solved.

    The error happens when authorizing the channel, not when authenticating the request using auth:api middleware.

    My private channel authorizing function in routes/channels.php always returns false meaning it will reject all subscribing requests to private-device.{device_id} channel:

    Broadcast::channel('device.{device_id}', function ($device, $device_id) {
        // this always return false, because of inequality of types
        return $device->id === $device_id;
    });
    

    Authorizing function above always return false, because of inequality of types between $device->id (which is of type int) and $device_id (which is of type string).

    So, in order to solve the problem, I cast both of them to int and then checked for equality.

    Here's the code I used to solve the problem:

    Broadcast::channel('device.{device_id}', function ($device, $device_id) {
        return (int) $device->id === (int) $device_id;
    });