Open4

GraphQLの認証認可、どこでやるのがいいのか?

yoshihiro nakamurayoshihiro nakamura

@auth Custom Directive

カスタムディレクティブを使う方法。スクラッチ実装よりも宣言的でクリーンな気はする。この場合スキーマに認証認可ルールが露出する。

https://www.apollographql.com/docs/apollo-server/security/authentication/#with-custom-directives

const typeDefs = `
  directive @auth(requires: Role = ADMIN) on OBJECT | FIELD_DEFINITION

  enum Role {
    ADMIN
    REVIEWER
    USER
  }

  type User @auth(requires: USER) {
    name: String
    banned: Boolean @auth(requires: ADMIN)
    canPost: Boolean @auth(requires: REVIEWER)
  }
`
yoshihiro nakamurayoshihiro nakamura

Graphql Shield

https://www.graphql-shield.com/docs/shield

APIが完結でわかりやすい

const permissions = shield({
  Query: {
    frontPage: not(isAuthenticated),
    fruits: and(isAuthenticated, or(isAdmin, isEditor)),
    customers: and(isAuthenticated, isAdmin),
  },
  Mutation: {
    addFruitToBasket: isAuthenticated,
  },
  Fruit: isAuthenticated,
  Customer: isAdmin,
})

schema = applyMiddleware(schema, permissions)

ルールの例。ctxに詰めた情報でbooleanを返すだけ。

const isAdmin = rule()(async (parent, args, ctx, info) => {
  return ctx.user.isAdmin
})
yoshihiro nakamurayoshihiro nakamura

envelopでuseGenericAuthを使う

3パターンくらいの実装ができるが、authディレクティブを使うならこの組み合わせは良さそう

import { envelop } from '@envelop/core';
import { useGenericAuth, resolveUser, ValidateUserFn } from '@envelop/generic-auth';

type UserType = {
  id: string;
};
const resolveUserFn: ResolveUserFn<UserType> = async context => {
  /* ... */
};
const validateUser: ValidateUserFn<UserType> = async (user, context) => {
  /* ... */
};

const getEnveloped = envelop({
  plugins: [
    // ... other plugins ...
    useGenericAuth({
      resolveUser,
      validateUser,
      mode: 'protect-auth-directive',
    }),
  ],
});

mode: 'protect-auth-directive'を設定すると、authディレクティブを使うフィールドにはvalidateUser関数を適用するようになる。
他に、validateUserを全フィールドに適用するprotect-all、何もせずにUserをinjectするだけのresolve-onlyがある。

authディレクティブを使う場合のvalidateUser:

import { ValidateUserFn } from '@envelop/generic-auth';

const validateUser: ValidateUserFn<UserType> = async (user, context, { root, args, context, info }, directiveNode) => {

  if (!user) {
    throw new Error(`Unauthenticated!`);
  }

  const valueNode = authDirectiveNode.arguments.find(arg => arg.name.value === 'role').value as EnumValueNode;
  const role = valueNode.value;

  if (role !== user.role) {
    throw new Error(`No permissions!`);
  }
};