Laravel Lighthouse Graphql multiple guards - laravel

I am trying to make my custom guard to work against a GraphQL schema with Lighthouse but its not working.
The problem is that my custom guard is not reached by graphql query.
The schema.graphql: (removed some code for brevity)
extend type Query #guard(with: ["api", "partner"])
{
GetHighscores(): [Highscore!]!
}
Note the #guard directive, it has api and partner. I want they run in that order (btw does it have order?).
Now the definition of the partner guard
AuthServiceProvider#boot
Config::set('auth.guards.partner', [
'driver' => 'partner',
]);
Auth::viaRequest('partner', function (Request $request) {
dd('MUST GET HERE');
});
When I run the query using an authenticated bearer token, the guard partner is not executed. But if I set the partner guard before api then it gets executed. What am I missing here?
edit
Using CURL, I have requested a protected laravel route and it works:
Route::middleware(['auth:api', 'auth:partner'])->get('/partners', function (Request $request) {
print_r($request->user()->toArray());
});
it printed the dd() message, which is right.
So why Lighthouse could not reached my custom guard?

I solved my issue moving the check for the API guard inside the partner guard:
Auth::viaRequest('partner', function (Request $request) {
$user = auth()->user();
if ($user &&
$user->partner()->exists() === true &&
auth()->guard('api')->check() === true
) {
return $user;
}
});
in the graphql schema:
extend type Query #guard(with: "partner")
{
GetHighscores(): [Highscore!]!
}
I still thinking that Lighthouse should be take care of multiple guards like it was an "AND"... anyways, its working now.

The lighthouse #guard directive behaves like a OR directive.
If the first guard passes, the second one is never checked.
What you could to is add the #guard directive twice. I think that should work.
extend type Query #guard(with: ["api"]) #guard(with: ["partner"])
{
GetHighscores(): [Highscore!]!
}
This is the code from the directive:
/**
* Determine if the user is logged in to any of the given guards.
*
* #param array<string> $guards
*
* #throws \Illuminate\Auth\AuthenticationException
*/
protected function authenticate(array $guards): void
{
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
$this->auth->shouldUse($guard);
return;
}
}
$this->unauthenticated($guards);
}

My use case may be different as I wanted authorization not multiple kinds of authentication, I have users that should be allowed to authenticate but need to do some verification mutations and limited querying and be admitted to use the rest of the API by an administrator.
Was thinking to just copy the guard directive and make it work as an and?
But I ended up creating a FieldMiddleware directive as I did not want other checks to need to do a separate authentication or copy same auth guard everywhere as that could be subject to change, I just wanted to check some attributes from the model were okay for a user to continue to use that query or mutation.
Although it does seem like #can and policy or gate would be more appropriate if not authenticating but checking some role/permission/state, I just wanted something I could use that was clear cut as I haven't made policy for absolutely every model yet a lot of them are just querying any data, using Policy would also apply for my back-end system which has different checks for admin already.
sail artisan lighthouse:directive AllowedInApp --field
1 FieldMiddleware
So the field Middleware can wrap and do checks I need and throw if not allowed.
<?php
namespace App\GraphQL\Directives;
use App\Models\User;
use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Exceptions\AuthorizationException;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
final class AllowedInAppDirective extends BaseDirective implements FieldMiddleware
{
public static function definition(): string
{
return /** #lang GraphQL */ <<<'GRAPHQL'
directive #allowedInApp on FIELD_DEFINITION
GRAPHQL;
}
/**
* Wrap around the final field resolver.
*
* #param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* #param \Closure $next
* #return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function handleField(FieldValue $fieldValue, Closure $next)
{
$previousResolver = $fieldValue->getResolver();
$fieldValue->setResolver(function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($previousResolver) {
/***
* #var User
*/
$user = $context->user();
if ($user->isVerifiedAndAdmitted === false) {
throw new AuthorizationException(
"You are not authorized to access {$this->nodeName()}"
);
}
return $previousResolver($root, $args, $context, $resolveInfo);
});
return $next($fieldValue);
}
}
In my user model I added an attribute that did the check I wanted, only have users but it is possible to have multiple kinds of authenticatable model actually.
/**
* Get verified status returns true if both mobile and email are verified and the user is admitted.
*
* #return \Illuminate\Database\Eloquent\Casts\Attribute
*/
public function isVerifiedAndAdmitted(): Attribute
{
return Attribute::make(
get: fn($value, $attributes) => isset($attributes['mobile_verified_at'])
&& isset($attributes['email_verified_at'])
&& $attributes['admitted_at'] !== null
&& now()->greaterThanOrEqualTo($attributes['admitted_at'])
);
}
From what I saw about the guard directive in lighthouse source.
The built in Guard Directive just returns when one defined guard or default guard succeeds.
src/Auth/GuardDirective.php
Untested, not sure its the right thing to use, maybe policy or gate is better.
/**
* Determine if the user is logged in to all of the given guards.
*
* #param array<string|null> $guards
*
* #throws \Illuminate\Auth\AuthenticationException
*/
protected function authenticate(array $guards): void
{
$canPass = true;
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
// #phpstan-ignore-next-line passing null works fine here
$this->auth->shouldUse($guard);
} else {
$canPass = false;
break;
}
}
if ($canPass) {
return;
}
$this->unauthenticated($guards);
}
```

Related

Laravel Nova only access from specific guard

In NovaServiceProvider there is:
protected function gate()
{
Gate::define('viewNova', function ($user) {
return in_array($user->email, [
'example#example.com',
]);
});
}
But what I would like to do is only allow people from the admins guard that I've setup in config/auth to access Nova. All users from the web guard should ideally get a 404 when they access any Nova URL.
This question for Telescope seems to be similar, but I can't seem to figure out where I should define this, and how to generate a 404 for the web guard.
A question that is probably related: what does viewNova in the gate method actually mean?
Can I define that specific action for a specific guard in config/auth? (I think I've seen this somewhere but can't seem to find it)?
There doesn't seem to be a Policy written for Nova?
Checkout vendor/laravel/nova/src/NovaApplicationServiceProvider.php. It has a method called authorization:
/**
* Configure the Nova authorization services.
*
* #return void
*/
protected function authorization()
{
$this->gate();
Nova::auth(function ($request) {
return app()->environment('local') ||
Gate::check('viewNova', [$request->user()]);
});
}
If the environment was local, it allows everyone to access the panel, but if the environment was something else, it checks for the definition on viewNova method and it passes the $request->user() to it.
In the same file, there's gate() method which defined viewNova:
/**
* Register the Nova gate.
*
* This gate determines who can access Nova in non-local environments.
*
* #return void
*/
protected function gate()
{
Gate::define('viewNova', function ($user) {
return in_array($user->email, [
//
]);
});
}
Basically, this method does nothing. You can implement it in app/Providers/NovaServiceProvider.php (which is the default implementation you see in the file and you've mentioned). In your case, you could implement it this way:
/**
* Register the Nova gate.
*
* This gate determines who can access Nova in non-local environments.
*
* #return void
*/
protected function gate()
{
Gate::define('viewNova', function ($user) {
Auth::guard('admin')->check();
});
}
It returns true if the currently authenticated user is in admin guard. Hope I could answer all your questions.

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']);
});
}
}

Apply Laravel 5.7 MustVerifyEmail on Multiple Authentication System

I'm trying to apply Laravel-5.7 MustVerifyEmail on multiple authentication system. So far what I've done is as follows:
created verification routes for the 'auditor' guard.
overwrite the show method in Verification controller with a new view.
Implemented a new notification in Auditor Model.
Created, register and applied a new middleware called 'auditor.verified'
After this procedure, I find that it's sending a notification to email and shows the verify page but when I click on the 'Verify Email Address' button in the mail it update the database with the timestamp but it don't take me to the redirect page. Instead, I get "The page isn't working" message in the browser.
There should be something I missed.
Here is the project file on GitHub
Thanks in advance for your help.
M.Islam's answer is a good one, but make sure to override the changes to EnsureEmailIsVerified instead of directly modified the source files. Otherwise your changes could be lost whenever you do $composer update or push to production.
Finally, after four days of research I was able to solve the issue.
I altered the "EnsureEmailIsVerified" middleware as follows:
<?php
namespace Illuminate\Auth\Middleware;
use Closure;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Auth;
class EnsureEmailIsVerified
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle($request, Closure $next, $guard = null)
{
$guards = array_keys(config('auth.guards'));
foreach($guards as $guard) {
if ($guard == 'admin') {
if (Auth::guard($guard)->check()) {
if (! Auth::guard($guard)->user() ||
(Auth::guard($guard)->user() instanceof MustVerifyEmail &&
! Auth::guard($guard)->user()->hasVerifiedEmail())) {
return $request->expectsJson()
? abort(403, 'Your email address is not verified.')
: Redirect::route('admin.verification.notice');
}
}
}
elseif ($guard == 'auditor') {
if (Auth::guard($guard)->check()) {
if (! Auth::guard($guard)->user() ||
(Auth::guard($guard)->user() instanceof MustVerifyEmail &&
! Auth::guard($guard)->user()->hasVerifiedEmail())) {
return $request->expectsJson()
? abort(403, 'Your email address is not verified.')
: Redirect::route('auditor.verification.notice');
}
}
}
elseif ($guard == 'web') {
if (Auth::guard($guard)->check()) {
if (! Auth::guard($guard)->user() ||
(Auth::guard($guard)->user() instanceof MustVerifyEmail &&
! Auth::guard($guard)->user()->hasVerifiedEmail())) {
return $request->expectsJson()
? abort(403, 'Your email address is not verified.')
: Redirect::route('verification.notice');
}
}
}
}
return $next($request);
}
}
And that's solved my problem.
So There was a similar question...
StackOverflow::Route [user.verification.notice] not defined / Override EnsureEmailIsVerified?
when using multiple guards you can just some guard redirection in the
App\Middleware\Authenticate.php
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
if (Arr::first($this->guards) === 'admin') {
return route('admin.login');
}
if (Arr::first($this->guards) === 'user') {
return route('user.login');
}
return route('login');
}
}
you can just add all the verify routes to your web.php file and change the named routes.
All auth routes can be found in
Illuminate\Routing\Router.php
\
/**
* Register the typical authentication routes for an application.
*
* #param array $options
* #return void
*/
public function auth(array $options = [])
{
// Authentication Routes...
$this->get('login', 'Auth\LoginController#showLoginForm')->name('login');
$this->post('login', 'Auth\LoginController#login');
$this->post('logout', 'Auth\LoginController#logout')->name('logout');
// Registration Routes...
if ($options['register'] ?? true) {
$this->get('register', 'Auth\RegisterController#showRegistrationForm')->name('register');
$this->post('register', 'Auth\RegisterController#register');
}
// Password Reset Routes...
if ($options['reset'] ?? true) {
$this->resetPassword();
}
// Email Verification Routes...
if ($options['verify'] ?? false) {
$this->emailVerification();
}
}
/**
* Register the typical reset password routes for an application.
*
* #return void
*/
public function resetPassword()
{
$this->get('password/reset', 'Auth\ForgotPasswordController#showLinkRequestForm')->name('password.request');
$this->post('password/email', 'Auth\ForgotPasswordController#sendResetLinkEmail')->name('password.email');
$this->get('password/reset/{token}', 'Auth\ResetPasswordController#showResetForm')->name('password.reset');
$this->post('password/reset', 'Auth\ResetPasswordController#reset')->name('password.update');
}
/**
* Register the typical email verification routes for an application.
*
* #return void
*/
public function emailVerification()
{
$this->get('email/verify', 'Auth\VerificationController#show')->name('verification.notice');
$this->get('email/verify/{id}/{hash}', 'Auth\VerificationController#verify')->name('verification.verify');
$this->post('email/resend', 'Auth\VerificationController#resend')->name('verification.resend');
}
So instead of using the Auth::routes() you would manually add these to your web.php routes file and just give them named routes.
Note: Once you change the named routes you will need to correctly reference them in your views.
The first thing it will complain about is the notifications mail which is referencing the default named routes...
You can over ride this in both the verification mail process and forgot password password by following the example here.
Forgot Password Custom Named Route and Email
To achieve this you would have to override the email notifications by creating two custom ones that override the two default ones.
You would emulate the laravel default structure found in the files
Illuminate\Auth\Notifications\VerifyEmail.php
Illuminate\Auth\Notifications\ResetPassword
Once you have created 2 notifications mailers.
eg
php artisan make:notification MailEmailVerificationNotification
which creates a file in App\Notifications\MailEmailVerificationNotification which effectively replicates the Illuminate\Auth\Notifications\VerifyEmail.php file
You will add the method to your model. Laravel default is User but if you are using custom guards with multiple tenant auth you would apply this to your relevant model.
You will then have the following on you model
/**
* Send the password reset notification.
* App\Notifications\MailResetPasswordNotification.php
*
* #param string $token
* #return void
*/
public function sendEmailVerificationNotification()
{
$this->notify(new MailEmailVerificationNotification());
}
Going this route is better because you override the Laravel default logic but you don't edit any Laravel specific files which means they won't get overwritten when updating Laravel and will only be affected when the is design changes like the recent move to extract the Laravel UI into its own package which changed things slightly on the passwords reset route.
you may note that we altered the App\Middleware\Authenticate file... this file is not part of vendor files and while provided to you as part of base install its left for you to alter update and change... the change we made was only to accommodate guards and not extensive change which allows for the multitenancy or not in the app.
For anyone I hope this helps and I went on a journey learning this and hope to reference this when I forget and hope it helps anyone walking a similar path.
I modified the middleware parameter in the __construct and email verification worked for me. I'm using laravel 6. posting the answer here though the question is old
public function __construct()
{
$this->middleware('auth:<your_guard>');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}

laravel 5.5. how to response a redirect from a action, not the route action

I want to check the auth before run the action. if the user deny, I will response the 403 status.
I use the method function in the __construct to check .
the code is following.
The User Controller:
public function __construct()
{
if (!app()->runningInConsole()) {
$beforeMethod = \Route::getCurrentRoute()->getActionMethod()."before";
if (method_exists($this, $beforeMethod)) {
return $this->$beforeMethod();
}
}
}
/**
* Edit interface.
*
* #param $id
* #return Content
*/
public function edit($id)
{
return "success";
}
/**
* Check user Auth
*
* #return \Illuminate\Http\RedirectResponse
*/
public function editBefore()
{
$id = \request()->route('user');
if (Gate::denies('edit', User::find($id))) {
return redirect("login");
}
}
the above code, don't return to the login.
what code should I use to achieve my purpose? Thanks!
You're really close to what I would do. I would use Policies.
You can add these to the routes or route groups so that you can check the policy before it hits the controller method whatsoever.
Create a new policy class that looks something like:
class UserPolicy
{
public function canEdit(User $user) // this user is the logged in user
{
// Here you return true or false depending on whether they can edit or not.
return $user->isAllowedToEdit();
}
}
Then you make sure the policy is added to AuthServiceProvider.php:
public function boot(GateContract $gate)
{
$gate->define('user.edit', UserPolicy::class.'#canEdit');
// Additional policies
}
Then make sure to add can to your $routeMiddleware in the Kernel.php:
protected $routeMiddleware = [
// other middleware
'can' => \Illuminate\Auth\Middleware\Authorize::class,
];
Finally, you can add it to your route making sure that you use the same name you defined in the AuthServiceProvider:
Route::get('/user/edit')
->uses('UserController#edit')
->middleware('can:user.edit');
I personally like doing it this way because it cleans up the controllers and lets the policies do all the work "behind the scenes". Even better, you can then use that same users.edit policy for multiple routes.

How to prevent access to Model by id with JWTAuth and Laravel?

I'm building some API endpoints with Laravel, and I'm using JWTAuth as the token provider for authorizing requests.
I've gotten the setup to protect a group of API routes that works correctly using:
Route::group(['prefix' => '/v1', 'middleware' => 'jwt.auth'], function() {
Route::resource('messages', 'MessagesController');
});
The Message model belongs to a User
I'm attempting to perform some requests using that relationship while keeping the request from providing data that didn't belong to the user:
Get a list of the logged in user's messages
Get an individual message of the logged in user
The main question I have is how to prevent the user from accessing a Message that does not belong to them. I have this in my controller:
public function show($message_id)
{
$message = Message::findOrFail($message_id);
return $message;
}
That obviously will return the Message regardless of whether or not it belongs to that user. Any suggestions on how to improve that to restrict accessing other user's messages?
For the listing of all messages, I was able to do the following in the controller:
public function index()
{
$user_id = Auth::User()->id;
$messages = Message::where('user_id', $user_id)->paginate(10);
return $messages;
}
This works, but I'm not sure this is the best way to do it. Maybe it is, but some feedback would be appreciated. I'm confused as to whether or not the middleware should be handling what the user has access to or if it should be part of the eloquent query?
Your question is,
how to prevent the user from accessing a Message that does not belong
to them.
Thats falls under Authorization
You can use Gates to Authorize users and I think it's the best approach here. Fist of all you'd need a Policy object.
<?php
namespace App\Policies;
use App\User;
use App\Message;
class MessagePolicy
{
/**
* Determine if the given Message can be viewed by the user.
*
* #param \App\User $user
* #param \App\Message $Message
* #return bool
*/
public function view(User $user, Message $Message)
{
return $user->id === $Message->user_id;
// Here this User's id should match with the user_id of the Massage
}
}
You can even generate the boilerplate like this,
php artisan make:policy MessagePolicy --model=Message
Then you can register it in your AuthServiceProvider like this,
<?php
namespace App\Providers;
use App\Message;
use App\Policies\MessagePolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
Message::class => MessagePolicy::class,
];
/**
* Register any application authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
Gate::define('view-message', 'MessagePolicy#view');
}
}
And in your Controller you can use it like this,
public function show($message_id)
{
if (Gate::allows('view-message', Message::findOrFail($message_id)) {
return $message;
}
}
Note: Please use this code snippet as a reference or a starting point since I haven't tested it properly. But underlying concept is correct. Use this as pseudocode. If something wrong found, please update it here :)

Resources