Laravel Passport - Testing Password Grant - php

I'm using Laravel 5.7 along with Passport to create an API for a first-party client. I have a login form that accepts the user's email and password and sends both to a custom LoginController. The LoginController then creates an oAuth payload, sends a POST request to oauth/token via Guzzle and returns the access_token, refresh_token and everything else to my first-party client.
Everything works perfectly when I test it in the browser. However I would now like to write an integration test for all of this and am running into an issue. The issue being that the oAuth server keeps rejecting my client and/or Guzzle request, only during testing.
Here is my corresponding code:
LoginController
<?php
namespace App\Http\Controllers\Api;
use App\Domain\Auth\PasswordGrant;
use App\Http\Requests\LoginRequest;
class LoginController extends ApiController
{
/**
* LoginController constructor.
*/
public function __construct()
{
$this->middleware('api')->only('login');
}
/**
* Attempt to authenticate the user with the credentials they provided
* and if successful, return an access token for the user.
*
* #param LoginRequest $request
* #return \Illuminate\Http\Response
*/
public function login(LoginRequest $request)
{
return PasswordGrant::attempt($request->email, $request->password);
}
}
PasswordGrant
<?php
namespace App\Domain\Auth;
use GuzzleHttp\Client as GuzzleHttp;
use GuzzleHttp\Exception\ClientException;
use Laravel\Passport\Client;
class PasswordGrant
{
/**
* The GuzzleHttp client instance.
*
* #var GuzzleHttp
*/
protected $http;
/**
* PasswordGrant constructor.
*
* #param GuzzleHttp $http
*/
public function __construct(GuzzleHttp $http)
{
$this->http = $http;
}
/**
* #param $username
* #param $password
* #return \Illuminate\Http\Response
*/
public static function attempt($username, $password)
{
$passwordGrant = resolve(static::class);
$payload = $passwordGrant->oAuthPayload(
$passwordGrant->oAuthClient(), $username, $password
);
return $passwordGrant->oAuthResponse($payload);
}
/**
* Get the oAuth Client we are using to authenticate our login and user.
*
* #return Client
*/
protected function oAuthClient()
{
return Client::query()
->where('name', config('api.password_client'))
->where('password_client', true)
->where('revoked', false)
->firstOrFail();
}
/**
* The payload we need to send to our oAuth server in order to receive
* a bearer token and authenticate the user.
*
* #param Client $client
* #param $username
* #param $password
* #return array
*/
protected function oAuthPayload(Client $client, $username, $password)
{
return [
'form_params' => [
'grant_type' => 'password',
'client_id' => $client->id,
'client_secret' => $client->secret,
'username' => $username,
'password' => $password,
'scope' => '*'
]
];
}
/**
* Get the response from our oAuth server.
*
* #param array $payload
* #return \Illuminate\Http\Response
*/
protected function oAuthResponse(array $payload)
{
try {
return $this->http->post(route('passport.token'), $payload)->getBody();
} catch (ClientException $exception) {
return response($exception->getMessage(), $exception->getCode());
}
}
}
PasswordGrantTest
<?php
namespace Tests\Feature\Requests\Team;
use App\Domain\Auth\PasswordGrant;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;
use Tests\TestCases\TestCase;
class PasswordGrantTest extends TestCase
{
use RefreshDatabase;
/** #test */
public function it_returns_an_access_token_for_a_user_with_valid_credentials()
{
Artisan::call('passport:client', [
'--password' => true,
'--name' => config('api.password_client')
]);
$user = create(User::class);
$result = PasswordGrant::attempt($user->email, 'secret');
dd($result);
}
}
The dd at the end of my test always returns a 401 with the message:
{"error":"invalid_client","message":"Client authentication failed"}
I have triple checked the existence and validity of my user model, the passport client and made sure the payload is well-formed.
Why does the password grant work when I test it via the browser but it does not work when making the same request to the server from my tests?
Perhaps I am missing certain headers in my request to the server during testing?

Related

Symfony oauth2 provider, Remove repeating code by 'stashing' new class variables within

I'm working on a big project that implements OAuth2 -> Specifically Oauth2-EveOnline
im using
PHP 8
Symfony 6
EveLabs/Oauth2
and as part of usage when creating the provider class the following code is required
$provider = new EveOnline([
'clientId' => $this->getParameter('eve.client_id'),
'clientSecret' => $this->getParameter('eve.client_secret'),
'redirectUri' => $this->getParameter('eve.redirect_uri'),
]);
What I would like to do is 'stash' this array within the EveOnline provider class so that in future all I have to call is
$provider = new EveOnline();
Unfortunate searching for an answer hasn't been fruitful, perhaps my searches are too vague, my problem is something that nobody has tried before or more likely it's simple and I am not experienced enough to see the solution
I have tried using a Service class to setup the provider but calling $this-> gets a little messy and I have been unable to work that out as $this calls the document your working on and to get .env details it should be done from the controller
I know that if I copy the provider from vendor/src to my App/src then it will override the vendor/src, if I can make changes to include my array and pull the info it needs then I can save myself passing the array on almost every page as I require the provider to manage tokens and communication with the EVE API (ESI).
Is this possible or is there a way I can move this to a Service?
When I learned how to create services I was rather happy and have created a few to handle all my API calls I just pass in my $provider and return the $response to my controller.
I was hoping to do something similar with building the provider, create a page builder service that requires 2 lines in the controller and handles setting up the provider and populating the session with info that I need to call on my pages, or use in DB/API queries.
I have already implemented a service for refreshing tokens but that is a very small inefficient feat.
App\Controller
<?php
namespace App\Controller;
use App\Service\TokenRefresh;
use Evelabs\OAuth2\Client\Provider\EveOnline;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
class Controller extends AbstractController
{
#[Route('/', name: 'page_homepage')]
public function homepage()
{
$session = new Session();
$session->start();
$provider = new EveOnline([
'clientId' => $this->getParameter('eve.client_id'),
'clientSecret' => $this->getParameter('eve.client_secret'),
'redirectUri' => $this->getParameter('eve.redirect_uri'),
]);
if($_SESSION['token']->hasExpired()){
$token = new TokenRefresh();
$token->newToken($provider);
}
if (isset($_SESSION)) {
dump($_SESSION);
}
return $this->render('homepage.html.twig');
}
#[Route('/login', name: 'page_login')]
public function login()
{
$session = new Session();
$session->start();
$scopes = [
'scope' => ['publicData', 'esi-markets.read_character_orders.v1', 'esi-markets.structure_markets.v1'] // array or string
];
$provider = new EveOnline([
'clientId' => $this->getParameter('eve.client_id'),
'clientSecret' => $this->getParameter('eve.client_secret'),
'redirectUri' => $this->getParameter('eve.redirect_uri'),
]);
if (!isset($_GET['code'])) {
// here we can set requested scopes but it is totally optional
// make sure you have them enabled on your app page at
// https://developers.eveonline.com/applications/
// If we don't have an authorization code then get one
$authUrl = $provider->getAuthorizationUrl($scopes);
$_SESSION['oauth2state'] = $provider->getState();
unset($_SESSION['token']);
header('Location: ' . $authUrl);
exit;
// Check given state against previously stored one to mitigate CSRF attack
} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {
unset($_SESSION['oauth2state']);
exit('Invalid state');
} else {
// In this example we use php native $_SESSION as data store
if (!isset($_SESSION['token'])) {
// Try to get an access token (using the authorization code grant)
$_SESSION['token'] = $provider->getAccessToken('authorization_code', [
'code' => $_GET['code']
]);
} elseif ($_SESSION['token']->hasExpired()) {
$token = new TokenRefresh();
$token->newToken($provider);
}
// Optional: Now you have a token you can look up a users profile data
try {
// We got an access token, let's now get the user's details
$user = $provider->getResourceOwner($_SESSION['token']);
// Use these details to create a new profile
} catch (\Exception $e) {
// Failed to get user details
exit('Oh dear...');
}
$user = $user->toArray();
$owner = [
'character_id' => $user['CharacterID'],
'character_name' => $user['CharacterName']
];
$session->set('owner', $owner);
$_SESSION['owner'] = $owner;
dump($user);
if (isset($_SESSION)) {
dump($_SESSION);
dump($session->get('owner'));
}
return $this->render('login.html.twig');
}
}
// TODO: Remove debug session clear
#[Route('/clrsession', name: 'app_clrsession')]
public function clrsession(){
$session = new Session();
$session->invalidate();
return $this->redirectToRoute('page_homepage');
}
}
Provider\EveOnline
<?php
namespace Evelabs\OAuth2\Client\Provider;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use Psr\Http\Message\ResponseInterface;
class EveOnline extends AbstractProvider
{
use BearerAuthorizationTrait;
/**
* Default scopes
*
* #var array
*/
public $defaultScopes = [];
/**
* Get the string used to separate scopes.
*
* #return string
*/
protected function getScopeSeparator()
{
return ' ';
}
/**
* Returns the base URL for authorizing a client.
*
* Eg. https://oauth.service.com/authorize
*
* #return string
*/
public function getBaseAuthorizationUrl()
{
return 'https://login.eveonline.com/oauth/authorize';
}
/**
* Returns the base URL for requesting an access token.
*
* Eg. https://oauth.service.com/token
*
* #param array $params
* #return string
*/
public function getBaseAccessTokenUrl(array $params)
{
return 'https://login.eveonline.com/oauth/token';
}
/**
* Returns the URL for requesting the resource owner's details.
*
* #param AccessToken $token
* #return string
*/
public function getResourceOwnerDetailsUrl(AccessToken $token)
{
return 'https://login.eveonline.com/oauth/verify';
}
/**
* Returns the default scopes used by this provider.
*
* This should only be the scopes that are required to request the details
* of the resource owner, rather than all the available scopes.
*
* #return array
*/
protected function getDefaultScopes()
{
return $this->defaultScopes;
}
/**
* Checks a provider response for errors.
*
* #throws IdentityProviderException
* #param ResponseInterface $response
* #param array|string $data Parsed response data
* #return void
*/
protected function checkResponse(ResponseInterface $response, $data)
{
//not throwing anything for 2xx responses
if (intval(substr($response->getStatusCode(), 0, 1)) === 2) {
return;
}
$message = $this->safeRead($data, 'error_description') || $this->safeRead($data, 'message');
throw new IdentityProviderException(
$message ?: $response->getReasonPhrase(),
$response->getStatusCode(),
$response
);
}
/**
* Generates a resource owner object from a successful resource owner
* details request.
*
* #param array $response
* #param AccessToken $token
* #return ResourceOwnerInterface
*/
protected function createResourceOwner(array $response, AccessToken $token)
{
return new EveOnlineResourceOwner($response);
}
/**
* Internal helper function to safe read from an array
* #param mixed $array
* #param string|int $key
* #return null
*/
private function safeRead($array, $key)
{
return !empty($array[$key]) ? $array[$key] : null;
}
}

Where can I get individual files for my Laravel Framework?

I am working with an inherited site based on the Laravel Framework which I upgraded from 5.6 to 8.0. I most aspects the site works great, but I occasionally sumble upon missing pieces. For example, I just discovered that the Reset Password feature does not work. Looking into it I find that there is a route for this:
Route::post('password/reset/{token}', ['as' => 'app.password.reset.post', 'uses' => 'App\Auth\ResetPasswordController#reset']);
Yet there is no 'reset()' method in the ResetPasswordController. Additionally, the ResetPasswordController uses the trait 'ResetsPassword', yet there is no such trait located under
Illuminate\Foundation\Auth\ResetsPasswords;
I tried checking the github repo for the Laravel framework, but these pieces were not there. I also looked under laravel-ui and didn't see them. According to the documentation,
"Laravel includes Auth\ForgotPasswordController and Auth\ResetPasswordController classes that contains the logic necessary to e-mail password reset links and reset user passwords. All of the routes needed to perform password resets may be generated using the laravel/ui Composer package"
I'm a little nervous about doing a general update as all other pieces are in place and working so I was looking for a way to obtain the individual pieces and have not found anything.
Here are my login routes:
Route::group(['prefix' => 'app'], function () {
//Auth::routes();
Route::get('login', ['as' => 'app.login', 'uses' => 'App\Auth\LoginController#showLoginForm']);
Route::post('login', ['as' => 'app.login.post', 'uses' => 'App\Auth\LoginController#login']);
Route::post('logout', ['as' => 'app.logout.post', 'uses' => 'App\Auth\LoginController#logout']);
Route::post('password/email', ['as' => 'app.password.email.post', 'uses' => 'App\Auth\ForgotPasswordController#sendResetLinkEmail']);
Route::get('password/reset', ['as' => 'app.password', 'uses' => 'App\Auth\ForgotPasswordController#showLinkRequestForm']);
Route::get('password/reset/{token}', ['as' => 'app.password.reset', 'uses' => 'App\Auth\ResetPasswordController#showResetForm']);
Route::post('password/reset/{token}', ['as' => 'app.password.reset.post', 'uses' => 'App\Auth\ResetPasswordController#reset']);
And this is what my ResetPasswordController looks like:
namespace App\Http\Controllers\App\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Foundation\Auth\ResetsPasswords;
class ResetPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
/**
* Where to redirect users after password reset.
*
* #var string
*/
protected $redirectTo = '/';
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->redirectTo = route('app.dashboard');
$this->middleware('guest');
}
/**
* Display the password reset view for the given token.
*
* If no token is present, display the link request form.
*
* #param \Illuminate\Http\Request $request
* #param string|null $token
* #return \Illuminate\Http\Response
*/
public function showResetForm(Request $request, $token = null)
{
return view('app.auth.passwords.reset')->with(
['token' => $token, 'email' => $request->email]
);
}
}
Also, from what I've read there is possibly an updated reset.blade.php. My question is what is my best approach to fix the reset password bug?
This is the trait for reseting passwords, that I found in my project, I hope it can help you somehow.
<?php
namespace Illuminate\Foundation\Auth;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
trait ResetsPasswords
{
use RedirectsUsers;
/**
* Display the password reset view for the given token.
*
* If no token is present, display the link request form.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showResetForm(Request $request)
{
$token = $request->route()->parameter('token');
return view('auth.passwords.reset')->with(
['token' => $token, 'email' => $request->email]
);
}
/**
* Reset the given user's password.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
public function reset(Request $request)
{
$request->validate($this->rules(), $this->validationErrorMessages());
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$response = $this->broker()->reset(
$this->credentials($request), function ($user, $password) {
$this->resetPassword($user, $password);
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $response == Password::PASSWORD_RESET
? $this->sendResetResponse($request, $response)
: $this->sendResetFailedResponse($request, $response);
}
/**
* Get the password reset validation rules.
*
* #return array
*/
protected function rules()
{
return [
'token' => 'required',
'email' => 'required|email',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
];
}
/**
* Get the password reset validation error messages.
*
* #return array
*/
protected function validationErrorMessages()
{
return [];
}
/**
* Get the password reset credentials from the request.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
protected function credentials(Request $request)
{
return $request->only(
'email', 'password', 'password_confirmation', 'token'
);
}
/**
* Reset the given user's password.
*
* #param \Illuminate\Contracts\Auth\CanResetPassword $user
* #param string $password
* #return void
*/
protected function resetPassword($user, $password)
{
$this->setUserPassword($user, $password);
$user->setRememberToken(Str::random(60));
$user->save();
event(new PasswordReset($user));
$this->guard()->login($user);
}
/**
* Set the user's password.
*
* #param \Illuminate\Contracts\Auth\CanResetPassword $user
* #param string $password
* #return void
*/
protected function setUserPassword($user, $password)
{
$user->password = Hash::make($password);
}
/**
* Get the response for a successful password reset.
*
* #param \Illuminate\Http\Request $request
* #param string $response
* #return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
protected function sendResetResponse(Request $request, $response)
{
if ($request->wantsJson()) {
return new JsonResponse(['message' => trans($response)], 200);
}
return redirect($this->redirectPath())
->with('status', trans($response));
}
/**
* Get the response for a failed password reset.
*
* #param \Illuminate\Http\Request $request
* #param string $response
* #return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
protected function sendResetFailedResponse(Request $request, $response)
{
if ($request->wantsJson()) {
throw ValidationException::withMessages([
'email' => [trans($response)],
]);
}
return redirect()->back()
->withInput($request->only('email'))
->withErrors(['email' => trans($response)]);
}
/**
* Get the broker to be used during password reset.
*
* #return \Illuminate\Contracts\Auth\PasswordBroker
*/
public function broker()
{
return Password::broker();
}
/**
* Get the guard to be used during password reset.
*
* #return \Illuminate\Contracts\Auth\StatefulGuard
*/
protected function guard()
{
return Auth::guard();
}
}

Custom authentication through API

I have a laravel API project. I want to be able to send a login request and get back a token depending on some custom logic. I am not using a database so i cant use the default auth.
I have set up a provider called AuthCustomProvider.
namespace App\Providers;
use Auth;
use App\Authentication\UserProvider;
use Illuminate\Support\ServiceProvider;
class AuthCustomProvider extends ServiceProvider
{
/**
* Perform post-registration booting of services.
*
* #return void
*/
public function boot()
{
Auth::provider('custom_auth', function($app, array $config) {
return new UserProvider();
});
}
/**
* Register bindings in the container.
*
* #return void
*/
public function register()
{
//
}
}
I have then added this to the config/app.php file in the providers array:
'providers' => [
App\Providers\AuthCustomProvider::class,
Then i added my custom providers driver to the config/auth.php file in the providers array:
'providers' => [
'users' => [
'driver' => 'custom_auth',
],
],
As im not using a database, I took out the model property
Lastly I created a folder called App/Authentication which i put my UserProvider.php file in which is this:
<?php
namespace App\Authentication;
use Illuminate\Contracts\Auth\UserProvider as IlluminateUserProvider;
class UserProvider implements IlluminateUserProvider
{
/**
* #param mixed $identifier
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
// Get and return a user by their unique identifier
}
/**
* #param mixed $identifier
* #param string $token
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifier, $token)
{
// Get and return a user by their unique identifier and "remember me" token
}
/**
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param string $token
* #return void
*/
public function updateRememberToken(Authenticatable $user, $token)
{
// Save the given "remember me" token for the given user
}
/**
* Retrieve a user by the given credentials.
*
* #param array $credentials
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
// Get and return a user by looking up the given credentials
}
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param array $credentials
* #return bool
*/
public function validateCredentials(Authenticatable $user, array $credentials)
{
// Check that given credentials belong to the given user
}
}
So lastly i make a function on the login controller. This is what the api call goes to:
public function Login(Request $request)
{
$user = Consultant::lookup('UserId', 1);
//Returns collection of user details (user id, username etc)
//Logic will go here in the future
$logThemIn = true;
if ($logThemIn)
{
auth()->login($user);
//return oauth2 token
}
}
So this is where im at now, if i run this, im getting the error:
'Declaration of App\Authentication\UserProvider::updateRememberToken(App\Authentication\Authenticatable $user, $token) must be compatible with Illuminate\Contracts\Auth\UserProvider::updateRememberToken(Illuminate\Contracts\Auth\Authenticatable $user, $token)'
Im new to laravel and there isnt alot of tutorials for what im trying to do that i can find. Any help is greatly appriciated
Change your UserProvider to this which uses Illuminate\Contracts\Auth\Authenticatable instead of App\Authentication\Authenticatable, php will load a class from the current namespace if one isn't specified.
<?php
namespace App\Authentication;
use Illuminate\Contracts\Auth\UserProvider as IlluminateUserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
class UserProvider implements IlluminateUserProvider
{
/**
* #param mixed $identifier
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
// Get and return a user by their unique identifier
}
/**
* #param mixed $identifier
* #param string $token
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifier, $token)
{
// Get and return a user by their unique identifier and "remember me" token
}
/**
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param string $token
* #return void
*/
public function updateRememberToken(Authenticatable $user, $token)
{
// Save the given "remember me" token for the given user
}
/**
* Retrieve a user by the given credentials.
*
* #param array $credentials
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
// Get and return a user by looking up the given credentials
}
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param array $credentials
* #return bool
*/
public function validateCredentials(Authenticatable $user, array $credentials)
{
// Check that given credentials belong to the given user
}
}
You forgot to import Authenticatable. Just add:
use Illuminate\Contracts\Auth\Authenticatable;

Laravel Echo - Allow guests to connect to presence channel

I am using laravel-echo-server to run Laravel Echo to broadcast events.
I have a user counter channel which shows all the users on the app. For this I am using a presence channel. This works fine for logged in users, but guests just never get connected.
I've setup the below in the BroadcastServiceProvider:
Broadcast::channel('global', function () {
return ['name' => 'guest'];
});
Which from what I can tell, should allow everyone in as 'guests'. I'm guessing there's some middleware or auth that's being checked before this that I need to disable for this channel.
Any help on getting all clients joining this presence channel would be much appreciated!
The other solutions didn't work for me for a guest presence channel, this is what I ended up with:
// routes/channels.php
<?php
use Illuminate\Auth\GenericUser;
/*
|--------------------------------------------------------------------------
| Broadcast Channels
|--------------------------------------------------------------------------
|
| Here you may register all of the event broadcasting channels that your
| application supports. The given channel authorization callbacks are
| used to check if an authenticated user can listen to the channel.
|
*/
Route::post('/custom/broadcast/auth/route', function () {
$user = new GenericUser(['id' => microtime()]);
request()->setUserResolver(function () use ($user) {
return $user;
});
return Broadcast::auth(request());
});
Broadcast::channel('online.{uuid}', function ($user, $uuid) {
return [
'id' => $user->id,
'uuid' => $uuid
];
});
You may create a temporary user with factory(User::class)->make(...) and authenticate it with a middleware to use it as a guest.
Step 1: Creating the middleware
Run: php artisan make:middleware AuthenticateGuest
In app/Http/Middleware/AuthenticateGuest.php:
public function handle($request, Closure $next)
{
Auth::login(factory(User::class)->make([
'id' => (int) str_replace('.', '', microtime(true))
]));
return $next($request);
}
Now setup the AuthenticateGuest middleware in Kernel.php.
In app\Http\Kernel.php:
protected $routeMiddleware = [
...
'authenticate-guest' => \App\Http\Middleware\AuthenticateGuest::class,
];
Step 2: Setup Broadcast::channel route
In routes/channels.php:
Broadcast::channel('chatroom', function ($user) {
return $user; // here will return the guest user object
});
More at: https://laravel.com/docs/8.x/broadcasting#authorizing-presence-channels
For anyone looking for answers to this. It is indeed possible to auth guests into presence channels you just need to override the Broadcast::routes() from the service provider with your own.
As an example my presence channel 'global' accepts guests:
Route::post('/broadcasting/auth', function(Illuminate\Http\Request $req) {
if($req->channel_name == 'presence-global'){return 'global';}
return abort(403);
});
This could be extended in various directions, or could continue to pass other presence and private channels through to the default Broadcast::auth method
You can create your own auth guard and it's also pretty simple but more complex.
Create a class which will implement Authenticable Interface.
Create UserProvider.
Create a new Guard.
Register Guard and UserProvider in AuthServiceProvider.
Add provider and guard in config/auth.php
Use your new guard.
Advantages
You don't have to modify auth endpoint
You don't have to change default guard
You base on Laravel Auth system
Keep support of multiple tabs in the browser
Can be used with web guard at the same time
Keep all the advantages of using PresenceChannel
Disadvantages
A lot to code
So,
1. Create a new class which will implement Authenticable interface.
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use JsonSerializable;
/**
* #property string $id
* #property string $name
*/
class Session implements Authenticatable, Jsonable, Arrayable, JsonSerializable
{
private $id;
private $attributes = [];
public function __construct($id)
{
$this->id = $id;
$this->name = "Guest";
}
/**
* Get the name of the unique identifier for the user.
*
* #return string
*/
public function getAuthIdentifierName()
{
return 'id';
}
/**
* Get the unique identifier for the user.
*
* #return mixed
*/
public function getAuthIdentifier()
{
return $this->{$this->getAuthIdentifierName()};
}
/**
* Get the password for the user.
*
* #return string
*/
public function getAuthPassword()
{
return "";
}
/**
* Get the token value for the "remember me" session.
*
* #return string
*/
public function getRememberToken()
{
return $this->{$this->getAuthIdentifierName()};
}
/**
* Set the token value for the "remember me" session.
*
* #param string $value
* #return void
*/
public function setRememberToken($value)
{
$this->{$this->getRememberToken()} = $value;
}
/**
* Get the column name for the "remember me" token.
*
* #return string
*/
public function getRememberTokenName()
{
return "token";
}
public function __get($name)
{
return $this->attributes[$name];
}
public function __set($name, $value)
{
$this->attributes[$name] = $value;
}
/**
* Convert the object to its JSON representation.
*
* #param int $options
* #return string
*/
public function toJson($options = 0)
{
return json_encode($this);
}
/**
* Get the instance as an array.
*
* #return array
*/
public function toArray()
{
return $this->attributes;
}
/**
* Specify data which should be serialized to JSON
* #link https://php.net/manual/en/jsonserializable.jsonserialize.php
* #return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* #since 5.4.0
*/
public function jsonSerialize()
{
return $this->attributes;
}
}
Modify this as you wish, but you shouldn't serialize $id property
2. Create UserProvider
<?php namespace App\Extensions;
use App\Models\Session;
use Illuminate\Cache\Repository;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Support\Fluent;
use Illuminate\Support\Str;
class SessionUserProvider implements UserProvider
{
private $store;
/**
* SessionUserProvider constructor.
* #param Repository $store
*/
public function __construct(Repository $store)
{
$this->store = $store;
}
/**
* Retrieve a user by their unique identifier.
*
* #param mixed $identifier
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
return new Session(
$this->getUniqueTokenForSession($identifier)
);
}
/**
* Retrieve a user by their unique identifier and "remember me" token.
*
* #param mixed $identifier
* #param string $token
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifier, $token)
{
return null;
}
/**
* Update the "remember me" token for the given user in storage.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param string $token
* #return void
*/
public function updateRememberToken(Authenticatable $user, $token)
{
return;
}
/**
* Retrieve a user by the given credentials.
*
* #param array $credentials
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
return null;
}
private function unpack($data)
{
return json_decode($data);
}
private function getUniqueTokenForSession($id)
{
return $this->retrieveCacheDataForSession($id)
->get('uuid');
}
private function retrieveCacheDataForSession($id)
{
$fluent = new Fluent(
$this->unpack(
$this->store->has($id) ? $this->store->get($id) : "[]"
)
);
if(!$fluent->__isset('uuid')) {
$fluent->__set('uuid', Str::random(128));
}
$this->store->put($id, $fluent->toJson(), 60 * 60 * 60);
return $fluent;
}
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param array $credentials
* #return bool
*/
public function validateCredentials(Authenticatable $user, array $credentials)
{
return null;
}
}
Identifier property in retrieveById method is always session id if you are using broadcasting so you can also use this as a token.
3. Create new Guard
<?php namespace App\Services\Auth;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;
class GuestGuard implements Guard
{
private $user;
protected $request;
protected $provider;
/**
* GuestGuard constructor.
* #param UserProvider $provider
* #param Request $request
*/
public function __construct(UserProvider $provider, Request $request)
{
$this->provider = $provider;
$this->request = $request;
}
/**
* Determine if the current user is authenticated.
*
* #return bool
*/
public function check()
{
return !is_null($this->user);
}
/**
* Determine if the current user is a guest.
*
* #return bool
*/
public function guest()
{
return !$this->check();
}
/**
* Get the currently authenticated user.
*
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
if($this->check()) {
return $this->user;
}
$this->setUser(
$this->provider->retrieveById(
$this->request->session()->getId()
)
);
return $this->user;
}
/**
* Get the ID for the currently authenticated user.
*
* #return int|null
*/
public function id()
{
return !is_null($this->user) ? $this->user->id : null;
}
/**
* Validate a user's credentials.
*
* #param array $credentials
* #return bool
*/
public function validate(array $credentials = [])
{
return false;
}
/**
* Set the current user.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #return void
*/
public function setUser(Authenticatable $user)
{
$this->user = $user;
}
}
Here in user method you pass session id as identifier, using broadcasting only this method is nessesary.
4. Register Guard and UserProvider in AuthServiceProvider.
// app/Providers/AuthServiceProvider.php
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
Auth::provider('sessions', function (Application $app) {
return new SessionUserProvider(
$app->make('cache.store')
);
});
Auth::extend('guest', function (Application $app, $name, array $config) {
return new GuestGuard(Auth::createUserProvider($config['provider']), $app->make('request'));
});
}
5.1 Add provider in config/auth.php
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
// New
'sessions' => [
'driver' => 'sessions',
'model' => App\Models\Session::class,
],
],
5.2 Add guard in config/auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
// New
'guest' => [
'driver' => 'guest',
'provider' => 'sessions'
]
],
6. Use your new guard
// routes/channels.php
Broadcast::channel('chat.{id}', function (Authenticatable $user){
return $user;
}, ['guards' => ['guest']]);
Notice that you can use 'web' as a guard at the same time ('web' should be before 'guest'). It allows you to find out who is a guest and who is a logged in user - you can just check instance of Authenticable in channel callback.
And that how it looks in the laravel-echo-server database
With the help of Renan Coelho i got it to work. The missing part for me was to override the Broadcast::routes() method with the following:
Route::post('/broadcasting/auth', function (Illuminate\Http\Request $req) {
return Broadcast::auth($req);
});
Route::post('/broadcasting/auth'... is actually a route that gets added through the "Broadcast::routes()" method. This is why we override it here. You can see the active routes by typing php artisan route:list in your terminal.
Then, Renan Coelho already said, i had to add a custom Middleware (AuthenticateGuest) that creates a random user for me. (This is the hacky part) and add it to the $middleware array in the kernel.php:
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
\Barryvdh\Cors\HandleCors::class,
\App\Http\Middleware\AuthenticateGuest::class
];
The AuthenticateGuest Middleware looks like the following:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use App\User;
class AuthenticateGuest
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
Auth::login(factory(User::class)->make([
'id' => (int)str_replace('.', '', microtime(true))
]));
return $next($request);
}
}
Hope that helps someone,
Sebastian
My solution to issue:
BroadcastServiceProvider.php (~/app/Providers/)
public function boot()
{
if (request()->hasHeader('V-Auth')) { /* Virtual client. */
Broadcast::routes(['middleware' => 'client_chat.broadcast.auth']);
} else {
Broadcast::routes();
}
require base_path('routes/channels.php');
}
Kernel.php (~/app/Http/)
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
...
'client_chat.broadcast.auth' => \App\Http\Middleware\ClientChatBroadcasting::class,
];
ClientChatBroadcasting.php (~/app/Http/Middleware/)
public function handle($request, Closure $next)
{
if (/** your condition **/) {
$fakeUser = new User;
$fakeUser->virtual_client = true;
$fakeUser->id = /** whatever you want **/;
$fakeUser->name = '[virtual_client]';
$fakeUser->asdasdasdasdasd = 'asdasdasdasdasd';
$request->merge(['user' => $fakeUser]);
$request->setUserResolver(function () use ($fakeUser) {
return $fakeUser;
});
}
return $next($request);
}
ChatChannel.php (~/app/Broadcasting/Chat/)
Broadcast::channel('chat.{chatId}', ChatChannel::class); Channel Classes
public function join($member/**($fakeUser)**/, $chatId)
{
$memberData = [/** your data **/];
/* If there is no member data (null), then there will be an authentication error. */
return $memberData;
}
[place in your js file, where you want connect to broadcasting]
this.Echo = new Echo({
broadcaster: 'socket.io',
host: /** your host **/,
reconnectionAttempts: 60,
encrypted: true,
auth: {
headers: {
'V-Auth': true,
'Access-Token': accessToken,
'Virtual-Id': virtualId,
'Chat-Id': chatId
}
}
});
I made it here reading this blog, getting the /broadcasting/auth error. The answers were very helpful but I didn't find the complete answer I needed.
I did this in Laravel 9.x so FYI it will work in that version too.
The major missing piece of code I needed was in the routes/web.php:
use Illuminate\Support\Facades\Route;
Broadcast::routes([
'middleware' => ['web', 'authenticate-guest'],
]);
Route::get('/', function () {
return view('welcome');
});
However I will add the rest down here in case anyone needs the whole thing:
Remove comment in app.config provider:
App\Providers\BroadcastServiceProvider::class,
Make middleware: php artisan make:middleware AuthenticateGuest.php
Then update it, in app/Http/Middleware
public function handle(Request $request, Closure $next)
{
Auth::login(User::factory()->make([
'id' => (int) str_replace('.', '', microtime(true))
]));
return $next($request);
}
app/Http/Kernel.php File..
protected $routeMiddleware = [
...
'authenticate-guest' => \App\Http\Middleware\AuthenticateGuest::class
];
app/Providers/BroadcastServiceProvider.php file should be this, but check:
public function boot()
{
Broadcast::routes();
require base_path('routes/channels.php');
}
Lastly, the routes/channel.php file:
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('common_room', function ($user) {
return true;
});
To finish everything off, do a npm run dev, followed by php artisan route:clear and maybe php artisan cache:clear/php artisan config:clear as well. Hope that helps!

Restler OAuth2 server

I have server login to my site. I use for API Restler 3 with OAuth2.
Server.php:
<?php
namespace Auth;
use Luracast\Restler\iAuthenticate;
use OAuth2\GrantType\UserCredentials;
use OAuth2\Storage\Pdo;
use OAuth2\Server as OAuth2Server;
use OAuth2\GrantType\AuthorizationCode;
use OAuth2\GrantType\ClientCredentials;
use OAuth2\Request;
use OAuth2\Response;
/**
* Class Server
*
* #package OAuth2
*
*/
class Server implements iAuthenticate
{
private $host = DB_HOST;
private $dbName = DB_NAME;
private $user = DB_LOGIN;
private $pass = DB_PASS;
/**
* #var OAuth2Server
*/
protected static $server;
/**
* #var Pdo
*/
protected static $storage;
/**
* #var Request
*/
protected static $request;
public function __construct()
{
$dns = "mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=utf8";
static::$storage = new PDO(
array('dsn' => $dns, 'username' => $this->user, 'password' => $this->pass)
);
$grantTypes = array(
'authorization_code' => new AuthorizationCode(static::$storage),
'user_credentials' => new UserCredentials(static::$storage),
);
static::$request = Request::createFromGlobals();
static::$server = new OAuth2Server(
static::$storage,
array('enforce_state' => true, 'allow_implicit' => true),
$grantTypes
);
$grantType = new ClientCredentials(static::$storage);
static::$server->addGrantType($grantType);
}
/**
* Stage 2: User response is captured here
*
* Success or failure is communicated back to the Client using the redirect
* url provided by the client
*
* On success authorization code is sent along
*
*
* #param bool $authorize
*
* #return \OAuth2\Response
*
* #format JsonFormat,UploadFormat
*/
public function postAuthorize($authorize = false)
{
static::$server->handleAuthorizeRequest(
static::$request,
new Response(),
(bool)$authorize
)->send();
exit;
}
/**
* Stage 3: Client directly calls this api to exchange access token
*
* It can then use this access token to make calls to protected api
*
* #format JsonFormat,UploadFormat
* #access public
* #url POST apiMobile/grand
* #url GET apiMobile/rer
*/
public function postGrant()
{
static::$server->handleTokenRequest(static::$request)->send();
exit;
}
/**
* Access verification method.
*
* API access will be denied when this method returns false
*
* #return boolean true when api access is allowed; false otherwise
*/
public function __isAllowed()
{
$token = static::$server->getAccessTokenData(Request::createFromGlobals());
global $idClient;
$idClient = $token['client_id'];
return self::$server->verifyResourceRequest(static::$request);
}
public function __getWWWAuthenticateString()
{
return 'auth string';
}
}
?>
And init.php:
<?php
use Luracast\Restler\Restler;
class ApiMode
{
private $class = '';
private $function = '';
public function __construct($controller = DEFAULT_CONTROLLER, $function = DEFAULT_FUNCTION)
{
$this->class = $controller;
$this->function = $function;
$controllerClass = ucfirst($this->class).CONTROLLER_TERMINAL;
$controllerPatch = CONTROLLER_DIR.'/'.$controllerClass.'.php';
require_once $controllerPatch;
require_once EXTERN_DIR.'/OAuth2/Autoloader.php';
OAuth2\Autoloader::register();
require_once EXTERN_DIR.'/vendor/restler.php';
require_once CLASS_DIR.'/Server.php';
$r = new Restler();
$r->addAuthenticationClass('Auth\\Server', '');
$r->setSupportedFormats('JsonFormat', 'XmlFormat');//,
$r->addAPIClass($controllerClass,'');
$r->setOverridingFormats('JsonFormat');
$r->setOverridingFormats('UploadFormat');
$r->handle();
}
}
?>
I use only Stage 3 - postGrand to get access token.
From web browser on http://mypage/apiMobile/rer.json (GET is for test from my web browser if GET work, POST work well) get:
local server Windows (this is OK):
{"error":"invalid_request","error_description":"The request method must be POST when requesting an access token","error_uri":"http:\/\/tools.ietf.org\/html\/rfc6749#section-3.2"}
web server (use https) Linux PHP 5.5.21 (fail):
{
"error": {
"code": 404,
"message": "Not Found"
},
"debug": {
"source": "Routes.php:438 at route stage",
"stages": {
"success": [
"get"
],
"failure": [
"route",
"negotiate",
"message"
]
}
}
}
On web server and local work all from API (address: mypage/apiMobile/myApi.json from controller):
$r->addAPIClass($controllerClass,'');
Main problem is with access to OAuth2 (I need access from http://mypage/apiMobile/rer.json). Any idea or tutorial?
Thanks.

Categories