Skip to content
This repository has been archived by the owner on Feb 13, 2022. It is now read-only.

GraphQL authorization #419

Open
gskierk opened this issue Sep 27, 2020 · 4 comments
Open

GraphQL authorization #419

gskierk opened this issue Sep 27, 2020 · 4 comments
Assignees

Comments

@gskierk
Copy link
Contributor

gskierk commented Sep 27, 2020

Hello,

What is recommended way to add authorization logic to Siler\GraphQL module? I'm aware of that authentication could be successfully done via route middleware, but the same cannot be done with authorization (or shouldn't... to achieve it anyway, you would have to parse request once more and fetch operation type, operation name and arguments recursively...).

For instance, package thecodingmachine/graphqlite offers @Security annotations that are handled by authentication and authorization services that you set in schema factory.

Are there any plans to implement similar feature to Siler\GraphQL? Or maybe solution already exists and I didn't manage to find it in documentation and source code? Maybe with dispatcher...?

I created actually a temporary workaround for this in Query class, but I wouldn't recommend it to anyone...

/**
 * @GraphQL\ObjectType(name="Query")
 */
class Query
{
    /**
     * @GraphQL\Field(name="user", description="Get user")
     * @GraphQL\Args(
     *     {
     *          @GraphQL\Field(name="id", type="Int")
     *     }
     * )
     */
    public static function getUser($root, array $args, $context, ResolveInfo $resolveInfo): User
    {
        return self::secure('_getUser', $args);
    }

    public static function secure(string $method, $arguments)
    {
        $user = \Siler\Container\retrieve(User::class);
        $request = \Siler\GraphQL\request()->toArray();

        $operationType = '...'; // check if operation type is 'query' or 'mutation', etc.
        $permissions = '...'; // load permissions for your role, specific $operationType and $method
        if (!isset($permissions['access']) || $permissions['access'] === false)
        {
            throw new Error('Access denied');
        }

        if (isset($acl['conditions']))
        {
            foreach ($acl['conditions'] as $condition)
            {
                // for example OwnUserCondition::class with method check($user, $args)
                // that verifies if $user->getId() === $args['id']
                
            }
        }

        return static::__callStatic($method, $arguments);
    }

    public static function _getUser($root, array $args, $context, ResolveInfo $resolveInfo): User
    {
        //...
    }
}
@leocavalcante
Copy link
Owner

Hi,
Actually, since there is no auth in the GraphQL spec, I haven't thought about a Siler provided solution for that, but we can came up with a solution! :)

On my APIs a use JWT then I add an User or a GuestUser to the GraphQL context then on each resolver I see in if there is a User the Context and if the User has proper roles.

@gskierk
Copy link
Contributor Author

gskierk commented Sep 27, 2020

So your solution is conceptually the same as mine. Repeating permission checking on each resolver doesn't sound good for me, that's why I mentioned GraphQLite solution.

Defining authorization logic inside the resolver is fine when learning GraphQL or prototyping. However, for a production codebase, delegate authorization logic to the business logic layer. - Authorization | GraphQL

I don't know the source code of Siler good enough, but I would create new server directive @auth(conditions, message, ...) and corresponding annotation @Authorization(conditions, message, ...) that would add @auth directive to proper places in generated schema. Then, somewhere, where $args, $context, etc. are already accessible (I don't know which Siler\GraphQL function would it be...), I would fetch authorization directive/annotation of current resolver and perform permission checking...

$user = $context['user'] ?? \Siler\Container\get('user'); // or any other way to get current user...
foreach ($authorization->getConditions() as $condition) {
    if (!$condition::check($user, $args)) {
        throw new SomeSuitableException($authorization->getMessage(), ..., $authorization->getSomething());
    }
}

@leocavalcante
Copy link
Owner

So, following the GraphQL's website recommendations:

Business Logic Layer
Your business logic layer should act as the single source of truth for enforcing business domain rules
Where should you define the actual business logic? Where should you perform validation and authorization checks? The answer: inside a dedicated business logic layer. Your business logic layer should act as the single source of truth for enforcing business domain rules.
In the diagram above, all entry points (REST, GraphQL, and RPC) into the system will be processed with the same validation, authorization, and error handling rules.

Auth shouldn't be on any part of GraphQL solution, it should be in your business rules/domain layer.


Anyway what do think about the Middleware pattern? Then you can define a pipeline and add it to run before reaching the resolver.

@gskierk
Copy link
Contributor Author

gskierk commented Oct 7, 2020

Inside standard middleware I have no direct access to $root, $args, $context, $resolveInfo variables, but middleware specifically for GraphQL would do the job.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants