GraphQLの認証と認可についてどうなっているのか知りたくなったので、調べてみた。
基本的にgraphql-rubyをベースに書いてます。
認証(Authentification)について
ここに記載されていた。
In general, authentication is not addressed in GraphQL at all.
残念ながら、認証の機構については全く用意されていなかった。
そのため認証については個別で実装したり、他のサードパーティのライブラリやSaaSなどを使用して導入するしかないっぽい。
ドキュメントには以下のように記載されていた。
Instead, your controller should get the current user based on the HTTP request (eg, an HTTP header or a cookie) and provide that information to the GraphQL query.
代わりにHTTPリクエストのCookieなり、ヘッダーなりにユーザーの情報を含めてGraphQLのやり取りすることが必要だと言っている。
実際、graphql-rubyをインストールした際に生成されるcontrollerはこのようになっている。
class GraphqlController < ApplicationController # If accessing from outside this domain, nullify the session # This allows for outside API access while preventing CSRF attacks, # but you'll have to authenticate your user separately # protect_from_forgery with: :null_session def execute variables = ensure_hash(params[:variables]) query = params[:query] operation_name = params[:operationName] context = { # Query context goes here, for example: # current_user: current_user <= ここがコメントアウトされている } result = AppSchema.execute(query, variables: variables, context: context, operation_name: operation_name) render json: result rescue StandardError => e raise e unless Rails.env.development? handle_error_in_development e end private # Handle form data, JSON body, or a blank value def ensure_hash(ambiguous_param) case ambiguous_param when String if ambiguous_param.present? ensure_hash(JSON.parse(ambiguous_param)) else {} end when Hash, ActionController::Parameters ambiguous_param when nil {} else raise ArgumentError, "Unexpected parameter: #{ambiguous_param}" end end def handle_error_in_development(e) logger.error e.message logger.error e.backtrace.join("\n") render json: { error: { message: e.message, backtrace: e.backtrace }, data: {} }, status: 500 end end
contextの部分にcurrent_userというものが用意されている。
実装者がこの部分を何らかの形でユーザー情報を取得し、current_userに含めれば、その後のQueryやMutationでcurrent_userにアクセスして 認可的な処理などを実装することができる。
それで認証は?
先ほどの説明から認証の機構は用意されていないので、自前で実装するしかない。
全てを1からやるのは面倒なので、railsの認証のライブラリで有名なDeviseを使用してサンプル実装をやってみた。
使用したgem
- devise
- devise-token_authenticatable
deviseの基本的な部分はをインストールすれば、モデルなど自動的に作成してくれるので特にやることはない。
先ほどの GraphqlController
のcontextのcurrent_userのコメントアウトを外せば使用できるようになり、
デバッグしてみてもリクエストを送信したユーザーの情報が含まれていることがわかると思う。
(リクエストにはユーザー作成時に発行されたトークンをリクエストの Authorization: Bearer <発行されたトークン>
にセットすることでユーザーが誰であるのかをサーバー側で把握することができる。)
認可について
graphql-rubyには3つの認可フレームワークが用意されている。
- visibility
- Accessibility
- Authorization
このうち Accessibility
については、非推奨となっているので今回は扱わない。
visibilityについて
これは、ユーザーの権限によってGraphQLスキーマの一部を隠すものだ。
実際にvisibilityを使用してみる
module Types class QueryType < Types::BaseObject field :todos, Types::TodoType.connection_type, null: false do def visible?(context) super && context[:current_user].role == 'admin' end end def todos Todo.all end end end
上記以外の補足説明としては以下の2つのモデルが存在している
- User
- Todo
class User < ApplicationRecord has_many :todos, dependent: :destroy end class Todo < ApplicationRecord belongs_to :user end
上記のような関係性である。
最初のQueryTypeの説明に戻すと、adminの場合はユーザー関係なく全てのTodoを見れるようにしたいとする。
上記を実現するためにvisibilityをfield: todosに実装した。
これの状態でadmin以外のユーザーでクエリを実行すると以下のように返ってくる。
"message": "Field 'todos' doesn't exist on type 'Query'"
そもそもそんなものないよと言われている。
確かに隠している形になっている。
ちなみにvisibilityはドキュメントに記載されているように4種類のメソッドを提供している
Type classes have a .visible?(context) class method
Fields and arguments have a #visible?(context) instance method
Enum values have #visible?(context) instance method
Mutation classes have a .visible?(context) class method
特定のモデルに紐付くTypeのfieldに使用したり、
mutationでもクラスメソッドして提供されているので、特定のmutationを隠蔽することができる。
例
module Mutations class CreateTodoMutation < BaseMutation argument :title, String, required: true argument :description, String, required: true field :todo_edge, Types::TodoType.edge_type, null: false def self.visible?(context) super && context[:current_user].role == 'admin' end # 以下省略 end
Authorizationについて
これは、ユーザーがアクセス中のオブジェクトに対してアクセスできる権限をチェックするものだ。
先ほどのvisibilityの例をauthorizationに変えてみた
module Types class QueryType < Types::BaseObject field :todos, Types::TodoType.connection_type, null: false do def authorized?(obj, args, ctx) super && context[:current_user].role == 'admin' end end def todos Todo.all end end end
visibilityは受け渡される引数はcontextのみだったのに対して、 object, argument, contextが受け渡される。
objectはfieldから返されるアプリケーションオブジェクトをあらわす。
argumentはアクセスするfieldに必要なargumentをあらわす。
クエリを実行すると、結果としてはvisibilityと同じようにアクセスできずエラーが返却されるが、 メッセージ内容が異なることがわかる。
"message": "Cannot return null for non-nullable field Query.todos"
QueryTypeのtodosというfieldに対して許可を持ってないので、非表示にしたという内容の記述が返ってきた。
先ほどのvisibilityはtodosなんてないよと言ってきたのに対して、こちらは存在するが権限を持っていないよと言われている。
Authorizationは3種類のメソッドを提供している
Type classes have .authorized?(object, context) class methods
Fields have #authorized?(object, args, context) instance methods
Arguments have #authorized?(object, arg_value, context) instance methods
先ほどと同じように実装することができる。
Authorizationはデフォルトでアクセスしたオブジェクトに対して、許可しなかった場合、オブジェクトが存在しなかったかのようにnilを返すようになっている。
これは実装者自信でカスタマイズすることができる。
class AppSchema < GraphQL::Schema mutation(Types::MutationType) query(Types::QueryType) # Opt in to the new runtime (default in future graphql-ruby versions) use GraphQL::Execution::Interpreter use GraphQL::Analysis::AST # Add built-in connections for pagination use GraphQL::Pagination::Connections def self.unauthorized_object(error) raise GraphQL::ExecutionError, "An object of type #{error.type.graphql_name} was hidden due to permissions" end def self.unauthorized_field(error) raise GraphQL::ExecutionError, "The field #{error.field.graphql_name} on an object of type #{error.type.graphql_name} was hidden due to permissions" end end
クラスメソッドとして提供されているunauthorized_fieldを実装することで、デフォルトではなく、カスタマイズしたものを返すことができる。
特定のfieldに対してはunauthorized_fieldが提供され、
特定の(クラス)オブジェクトに対してはunauthorized_objectが提供される。
例
module Types class QueryType < Types::BaseObject def self.authorized?(obj, ctx) super && ctx[:current_user].role == 'admin' end # 以下省略 end end
unauthorized_objectを実装していないと以下のようにnullが返ってくる
{ "data": null }
unauthorized_objectを実装すると以下のように変えることができる。
{ "errors": [ { "message": "An object of type Query was hidden due to permissions" } ] }