Nuxt websockets Laravel Echo private channel auth not triggered - websocket

Im currently working on implementing websockets in my Nuxt App. I have a Laravel backend and im using Pusher and Laravel Echo. The problem is that, when trying to connect/subscribe to a private channel - as the client is authorized via the broadcast/auth endpoint the individual channel auth (channels.php) is not hit. So it is possible for a logged in user to access a private channel that they should not be able to access.
My code/configuration is as follows:
NUXT FRONTEND:
nuxt.config.js
echo: {
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
forceTLS: process.env.NODE_ENV === 'production',
authModule: true,
authEndpoint: `${process.env.API_URL}/broadcasting/auth`,
connectOnLogin: true,
disconnectOnLogout: true,
auth: {
headers: {
'X-AUTH-TOKEN': process.env.API_AUTH_TOKEN
}
}
},
LARAVEL BACKEND:
BroadcastServiceProvider.php
public function boot()
{
Broadcast::routes(['middleware' => [JWTAuthMiddleware::class]]);
require base_path('routes/channels.php');
}
AuthController.php
public function auth(Request $request): JsonResponse
{
$pusher = new Pusher(
config('broadcasting.connections.pusher.key'),
config('broadcasting.connections.pusher.secret'),
config('broadcasting.connections.pusher.app_id')
);
$auth = $pusher->socket_auth($request->input('channel_name'), $request->input('socket_id'));
return ResponseHandler::json(json_decode($auth));
}
ChatMessageEvent.php
/**
* #inheritDoc
*/
public function broadcastOn()
{
return new PrivateChannel('chat.' . $this->chatMessage->getChatId());
}
channels.php
Broadcast::channel(
'chat.{chatId}',
function (JwtUserDTO $user, int $chatId) {
Log::info('test');
return false;
}
);
As you may have noticed, we use a JWT auth strategy which is stored on the client side - so we have no sessions. But as the authorization via the auth endpoint works it should be possible to guard the individual private channels via the channels.php routing ? But as i can see in the logs, it is never reached. Am i missing some configuration ? or why am i authorized solely on the auth endpoint and not also on the individual channels routes ?

After a lot of searching I found out that the issue was with my AuthController.php as I had implemented my own auth function - which made it work in order to authenticate the user to the private channel. Unfortunately this then resulted in not leviating the BroadcastServiceProvider. So the solution was:
use Illuminate\Broadcasting\BroadcastController;
Route::post('broadcasting/auth', [BroadcastController::class, 'authenticate'])
->middleware(BroadcastMiddleware::class);
This will then use the Broadcast Facade and enable use of the channels.php for authenticating the user against the given channel.
I also had to add a middleware to set the authenticated user in the Laravel session as this is needed by the ServiceProvider.
/**
* #param Request $request
* #param Closure $next
* #return mixed
*/
public function handle(Request $request, Closure $next)
{
/** #var JwtUserDTO $jwt */
$jwt = $request->get('jwt');
// Set the user in the request to enable the auth()->user()
$request->merge(['user' => $jwt]);
$request->setUserResolver(function() use ($jwt) {
return $jwt;
});
Auth::login($jwt);
return $next($request);
}
And to do this the Model or DTO in my case had to implement the Illuminate\Contracts\Auth\Authenticatable interface. Remember to add functionality for the getAuthIdentifierName and getAuthIdentifier to return the username and user id respectivly as this is also needed if you want to play with presences channels.

Related

Livewire session returns NULL in Laravel Auth Middleware

I have a livewire component that needs to send requests using JWT.
As the livewire GET request is not sending the token by default, I decided to modify the Laravel's Authentication middleware in order to send the token that I put in session in the livewire component, via bearer in Laravel Auth middleware.
That the variable that I added to the session in my livewire component, returns NULL when I try to retrieve it in the Authentication middleware.
1.Livewire component
<?php
namespace App\Http\Livewire;
class HelloWorld extends Component
{
public function mount(TransactionCheckoutRequest $request)
{
session(['x__livewire__token' => 'put token in session' ]);
}
}
2.Laravel auth middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Support\Facades\Cookie;
class Authenticate extends Middleware
{
public function handle($request, Closure $next, ...$guards)
{
# Check if request is made using LIVEWIRE
if ( request()->hasHeader('x-livewire') ) {
/**
* get token from session
*
*
* But, this $token return NULL
* instead token value.
*/
$token = session()->get('x__livewire__token');
$request->headers->set('Accept', 'application/json');
$request->headers->set('Content-Type', 'application/json');
// # Send token to request header
$request->headers->set('Authorization', 'Bearer ' . $token);
}
# After that, authenticate livewire request
$this->authenticate($request, $guards);
return $next($request);
}
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* #param \Illuminate\Http\Request $request
* #return string|null
*/
protected function redirectTo($request)
{
if ( !$request->expectsJson() ) {
return route('user.login');
}
}
}
3.Web route file
<?php
Route::group(['prefix' => 'v1'], function() {
Route::group(['middleware' => ['auth:api', 'jwt.verifyer', 'hostname.access.verifyer']], function() {
# Here is LIVEWIRE component
Route::get('/processing', HelloWorld::class)->name('transaction.checkout');
});
});
Can somebody explain how should I extend the middleware to use the token?
Livewire uses its own request when making requests. if you want to send additional data. You can send it using addHeaders.
<script type="text/javascript">
document.addEventListener("livewire:load", function(event) {
Livewire.addHeaders({
'Authorization': 'Bearer {{ $token }}',
})
});
</script>

Method Illuminate\Auth\RequestGuard::attempt does not exist. when trying custom authentication via web route

I have to authenticate users via an external api (something like ldap) and have been trying to realize authentication via a closure request guard as documented here https://laravel.com/docs/master/authentication#closure-request-guards
It works fine if the user logs in correctly, however on auth failure laravel throws the mentioned error https://laravel.com/docs/master/authentication#closure-request-guards if the failed attempt is returning null from the closure (as it says in the documentation). If it just returns false, laravel doesn't throw an error, however there is no validation feedback.
Auth::viaRequest('ldap', function ($request) {
$credentials = $request->only('login_id', 'password');
if ($user = ExternalLDPAAuth::auth()) {
return $user;
} else {
return null; // laravel throws error
// return false; // <- would not throw error, but no validation
}
}
Is there an easier way to do custom authentication?
I don't really understand the documentation about https://laravel.com/docs/5.7/authentication#authenticating-users
in the end I have to write the guard just like above anyway, right?
You haven't shown the code where you're calling attempt(), but you don't need to use that method when authenticating via the request. You use attempt() when a user attempts to login with credentials and you need to explicitly attempt to authenticate. With request authentication, Laravel will automatically attempt to authenticate as the request is handled, so your code can simply check to see if auth()->check() returns true or not.
In the end I decided to customize the EloquentUserProvider instead of the guard. In the end all i needed was additional logic to validate credentials and retrieve a user by credentials in case the user hadn't logged in yet. I.e. checking the normal eloquent logic first and then checking against the external API if nothing was found (also checking for case of changed password).
class CustomUserProvider extends EloquentUserProvider
{
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param array $credentials
* #return bool
*/
public function validateCredentials(UserContract $user, array $credentials)
{
// (...)
}
/**
* Retrieve a user by the given credentials.
*
* #param array $credentials
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
// (...)
}
}
// config/auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'custom',
],
// (...)
],
// providers/AuthServiceProvider.php
class AuthServiceProvider extends ServiceProvider
{
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
Auth::provider('custom', function ($app, array $config) {
return new CustomUserProvider($app['hash'], $config['model']);
});
}
}

How do I get the client id in a Laravel app?

I've set up a laravel app with client authentification. I send it my client id and client secret and it gives me a token. I'm able to log in to my laravel app, but I can't figure out how to get the id of the client that's been authorized.
I've seen hints to use auth()->user()->Token()->getAttribute('client_id') to get the client id, but since I'm only using clients there is no user and I get an error about trying to call Token() on a null object. Auth::id() also returned nothing. I grabbed the token from the header with Request::header('Authorization'), but it didn't match anything in the database.
I'm assuming you're using client credentials grant tokens, and the CheckClientCredentials middleware.
You can get this information from the bearer token, but it's not that straightforward. You would need to create a new PSR7 request with the token, and send it off to the oauth server to have it converted to readable data.
This is already done inside the CheckClientCredentials middleware provided by Passport. So, one way to do this would be to extend the CheckClientCredentials middleware and just manually set the needed fields on the request object from inside the middleware.
First, create app/Http/Middleware/MyCheckClientCredentials.php:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Auth\AuthenticationException;
use League\OAuth2\Server\Exception\OAuthServerException;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use Laravel\Passport\Http\Middleware\CheckClientCredentials;
class MyCheckClientCredentials extends CheckClientCredentials
{
/**
* The Resource Server instance.
*
* #var \League\OAuth2\Server\ResourceServer
*/
private $server;
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param mixed ...$scopes
* #return mixed
* #throws \Illuminate\Auth\AuthenticationException
*/
public function handle($request, Closure $next, ...$scopes)
{
$psr = (new DiactorosFactory)->createRequest($request);
try {
$psr = $this->server->validateAuthenticatedRequest($psr);
// This is the custom line. Set an "oauth_client_id" field on the
// request with the client id determined by the bearer token.
$request['oauth_client_id'] = $psr->getAttribute('oauth_client_id');
} catch (OAuthServerException $e) {
throw new AuthenticationException;
}
$this->validateScopes($psr, $scopes);
return $next($request);
}
}
Next, update your app/Http/Kernel.php to use your custom middleware instead of the build in Passport middleware:
protected $routeMiddleware = [
'client' => \App\Http\Middleware\MyCheckClientCredentials::class,
];
Apply the middleware to your route as normal:
Route::get('/user', function(Request $request) {
// Should show "oauth_client_id" field.
dd($request->all());
})->middleware('client');
If you don't want to do this inside a middleware, you can study how the Passport middleware works and reuse this code in some type of service if you'd like.
NB: all untested.
I had to do something similar in a logger middleware of mine:
.......................
$user = $request->user();
if($user) {
// assume the authorization header exists, since the user is authenticated
$header = $request->headers->get('authorization');
if($header) { // authorization header is not set when testing via Passport::actingAs()
/**
* Stolen from League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator#63
*/
// Get the actual jwt string from the header
$jwt = trim(preg_replace('/^(?:\s+)?Bearer\s/', '', $header));
// Parse the token from the string
$token = (new Lcobucci\JWT\Parser())->parse($jwt);
// Get the ID from the token
$oauthClientId = $token->getClaim('aud');
}
}
.......................

broadcasting using Pusher and Laravel Echo

I am going through this tutorial Introducing Laravel Echo. Broadcasting thing is working perfecting fine. Whenever I execute the command php artisan chat:message "something". Event get triggered and data stores in database. But moving on to Laravel Echo. I tried a lot but I am going no where. I have put Echo in /js/app.js but in docs it mentioned that /js/bootstrap.js. Since i am following the tutorial, therefore I have put it in /js/app.js Echo is not showing data in log. I am using ubuntu.
Test Event:
class TestEvent implements ShouldBroadcast
{
use InteractsWithSockets, SerializesModels;
public $message;
public $user;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct($user, $message)
{
//
$this->user = $user;
$this->message = $message;
}
/**
* Get the channels the event should broadcast on.
*
* #return Channel|array
*/
public function broadcastOn()
{
return "chat-room";
}
}
Send Chat Command Code:
class SendChatMessage extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'chat:message {message}';
protected $description = 'Send chat message.';
public function handle()
{
// Fire off an event, just randomly grabbing the first user for now
$user = \App\User::first();
$message = \App\Message::create([
'user_id' => $user->id,
'content' => $this->argument('message')
]);
event(new \App\Events\TestEvent($message, $user));
}
/**
* Create a new command instance.
*
* #return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* #return mixed
*/
}
Laravel Echo:
import Echo from "laravel-echo"
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'my-key'
});
window.Echo.channel('chat-room')
.listen('TestEvent', (e) => {
console.log(e.user, e.message);
});
one more thing whenever I send an event from pusher. Pusher is able to send it correctly.
Not sure if it is still relevant but might help someone.
I am using Laravel 5.4 and using 'eu' cluster in pusher. Please note that sometimes pusher goes crazy. It starts receiving broadcasted events late. So, I made my event implement ShouldBroadcastNow contract instead of ShouldBroadcast to overcome this problem. I am not using a queue to post events to pusher. Even then sometimes I notice delays in receiving events. Anyways, all well on the broadcasting part.
Now, on the client side, here is the script I am using to listen to events triggered from pusher(in my case private channel)
bootstrap.js (available by default in Laravel 5.4)
import Echo from "laravel-echo"
window.Echo = new Echo({
broadcaster: 'pusher',
key: '<your_pusher_app_key>',
csrfToken: window.Laravel.csrfToken,
cluster:'eu',
encrypted: true });
You should compile the JS if you make change to bootstrap.js else, your changes won't be packed into public folder. Laravel 5.4 uses Laravel Mix and the command to compile is "npm run dev"
In My blade template:
window.Echo.private('<channel_name_without_prefixing_private_keyword>')
.listen("<Just the event class name and not fully qualified name>", function(e){
console.log(e);
});
This worked for me. Further, ensure that, BroadcastServiceProvider is uncommented in config\app.php file. It's not enabled by default.
In routes/channels.php
Broadcast::channel('<channel_name_without_private_prefix>', function ($user) {
//Auth check
return true;
});

Sharing same route with unauthenticated and authenticated users in Laravel 5.3

I have a route that where I'm using an auth middleware and it works great.
Route::group(['prefix' => 'v1','middleware' => ['auth:api']], function()
{
Route::resource('user', 'v1\MyController');
});
The problem is that I would also like this route to be accessible to non-authenticated users as well. With the above, I get a 401 Unauthorized error and I can't return any content for unauthenticated users. So how can I authenticate this route (so it passes down the user data) while also allowing the route to proceed even if the user is NOT authenticated?
(I tried doing a conditional Auth check on the router page but it seems the user has gone through authentication yet so it always remains false.)
EDIT: I should also note that I'm using an API route with Password Grant & access tokens.
remove this route from current route group (which applies auth middleware).
then
public function __construct()
{
if (array_key_exists('HTTP_AUTHORIZATION', $_SERVER)) {
$this->middleware('auth:api');
}
}
then
if (Auth::check()) {
// Auth users
} else{
//Guest users
}
I am experiencing the same case.
since the auth middleware only checks for authenticated user, we can use client credentials for the non-authenticated user.
the client credentials have a separated middleware located in Laravel\Passport\Http\Middleware\CheckClientCredentails.
I have created a custom middleware to combine both middleware to allow either one is pass.
here is my custom middleware
namespace Laravel\Passport\Http\Middleware;
use Closure;
use League\OAuth2\Server\ResourceServer;
use Illuminate\Auth\AuthenticationException;
use League\OAuth2\Server\Exception\OAuthServerException;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
class CheckClientCredentials
{
/**
* The Resource Server instance.
*
* #var ResourceServer
*/
private $server;
/**
* Create a new middleware instance.
*
* #param ResourceServer $server
* #return void
*/
public function __construct(ResourceServer $server)
{
$this->server = $server;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*
* #throws \Illuminate\Auth\AuthenticationException
*/
public function handle($request, Closure $next, ...$scopes)
{
$psr = (new DiactorosFactory)->createRequest($request);
try{
$psr = $this->server->validateAuthenticatedRequest($psr);
} catch (OAuthServerException $e) {
throw new AuthenticationException;
}
foreach ($scopes as $scope) {
if (!in_array($scope,$psr->getAttribute('oauth_scopes'))) {
throw new AuthenticationException;
}
}
return $next($request);
}
}
Kernal.php
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.api' => \App\Http\Middleware\APIAuthenticate::class,
....
routes\api.php
Route::group([
'namespace' => 'API',
'middleware' => 'auth.api:api',
], function(){
....
From within an unauthenticated (not assigned the auth:api middleware) route's handler method, try:
Auth::guard("api")->user();
If it's populated, then your unguarded route can treat the access as authenticated. If not, its a random user accessing the route, and can be treated as such.
Dont put those urls whome you want to allow for both guest users and authenticated users in auth middleware. Because auth middleware allow for only authenticated users.
To check for authenticated and unauthenticated user you can use following code in view
#if (Auth::guest())
//For guest users
#else
//for authenticated users
#endif
Edited : In controller use
if (Auth::check()) {
// Auth users
} else{
//Guest users
}
#Yves 's answer is nearly correct. But a small change,
Instead of array_key_exists, we need to check whether key value is not null. Because It always has that key but null value. SO, instead of controller construct check for authorization header like this.
if ($_SERVER['HTTP_AUTHORIZATION']) {
$this->middleware('auth:sanctum');
}
Then you can check for authenticated user like this:
if (auth()->check()) {
// User is logged in and you can access using
// auth()->user()
} else {
// Unauthenticated
}
for sanctum use:
if (Auth::guard('sanctum')->check()) {
// user is logged in
/** #var User $user */ $user = Auth::guard('sanctum')->user();
} else {
// user is not logged in (no auth or invalid token)
}
You can use this example:
#if(Auth::user())
echo "authincatesd user";
#else
echo "unauthorised";
#endif

Resources