简体   繁体   中英

Laravel Lighthouse Graphql multiple guards

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);
    }
    ```

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM