0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GraphQL API における認証と認可の実装方法

Posted at

GraphQL を用いたアプリケーションでは、認証(Authentication)と認可(Authorization)の概念が重要です。認証はユーザーがシステムにログインしているか、または「認識されているか」を判断し、認可は認証されたユーザーが特定のリソースにアクセスできるかどうかを確認します。この二つは混同されることが多いですが、実際には異なるプロセスです。GraphQL での認証と認可の実装には少し工夫が必要ですが、適切に行うことでセキュアなアプリケーションを構築できます。

本記事では、GraphQL を用いた認証と認可の実装方法について解説します。

認証と認可の概要

認証(Authentication):

ユーザーがシステムにログインしているか、正当なユーザーであるかを確認します。GraphQL では、通常、ユーザーがリクエストを送信する際にトークン(JWTなど)を使って認証を行います。

認可(Authorization):

認証されたユーザーが、特定のリソースやアクションにアクセスできるかどうかを判断します。たとえば、あるユーザーが特定のレストランの予約をキャンセルする権限を持っているかどうかを確認します。

これらのプロセスは、GraphQL の単一のエンドポイント(例:/graphql)で行われるため、複雑に見えるかもしれませんが、適切な方法で実装することが可能です。

アプリケーション設定

まず、ユーザーのサインアップ(登録)とサインイン(ログイン)を実装します。以下に示すのは、基本的なユーザーモデルです。

from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from db import Base

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True, autoincrement=True)
    email = Column(String, unique=True)
    password = Column(String)
    table_bookings = relationship("TableBooking")

サインアップ(Sign Up)

ユーザーがメールアドレスとパスワードを使ってサインアップする処理を行います。

# api/graphql.py
import graphene
from service import sign_up

class SignUp(graphene.Mutation):
    class Arguments:
        email = graphene.String(required=True)
        password = graphene.String(required=True)

    user = graphene.Field(UserNode)
   
    def mutate(self, info, email: str, password: str):
        session = info.context["session"]
        user = sign_up(session, email, password)
        return SignUp(user=user)

sign_up 関数は新しいユーザーをデータベースに追加します。次に、サインインの処理に進みます。

サインイン(Sign In)

ユーザーがサインインすると、JWT(JSON Web Token)が発行され、以後のリクエストにおいて認証が行われます。

# api/graphql.py
import graphene
from service import sign_in

class SignIn(graphene.Mutation):
    class Arguments:
        email = graphene.String(required=True)
        password = graphene.String(required=True)

    token = graphene.String()

    def mutate(self, info, email: str, password: str):
        session = info.context["session"]
        token = sign_in(session, email, password)
        return SignIn(token=token)

sign_in 関数はユーザーの資格情報を確認し、JWT トークンを返します。

認証の実装

認証は、ユーザーが送信するリクエストの「Authorization」ヘッダーに含まれるトークンを使って行います。以下の sign_in_required デコレーターを使用して、GraphQL のフィールドリゾルバーで認証を行います。

# api/auth.py
from functools import wraps
from auth import get_user_by_token
from models import User

class UnauthenticatedUser(Exception):
    pass

def sign_in_required():
    def decorator(func):
        @wraps(func)
        def wrapper(root, info, *args, **kwargs):
            kwargs["current_user"] = get_current_user(info.context)
            return func(root, info, *args, **kwargs)
        return wrapper
    return decorator

def get_current_user(context) -> User:
    try:
        token = get_token_from_request(context["request"])
        user = get_user_by_token(context["session"], token)
        if not user:
            raise UnauthenticatedUser("UnauthenticatedUser")
        return user
    except KeyError:
        raise UnauthenticatedUser("UnauthenticatedUser")

def get_token_from_request(request) -> str:
    header = request.headers["Authorization"]
    token = header.replace("Bearer ", "", 1)
    return token

このデコレーターを用いることで、認証されていないユーザーは保護されたフィールドにアクセスできなくなります。

認可の実装

認可は、認証されたユーザーが特定のアクションを実行する権限を持っているかどうかを確認します。例えば、ユーザーが自分のレストラン予約をキャンセルすることは許可されますが、他のユーザーの予約をキャンセルすることは許可されません。

# api/auth.py
import re
from functools import wraps
from graphene.relay.node import from_global_id
from auth import authorize
from models import User

def camel_to_snake(name: str) -> str:
    """CamelCase -> camel_case"""
    return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()

class UnauthorizedAccess(Exception):
    pass

def authorize_required(model):
    def decorator(func):
        @wraps(func)
        def wrapper(root, info, *args, **kwargs):
            kwargs["current_user"] = get_current_user(info.context)
            model_name = model.__name__
            gid_field_name = f"{camel_to_snake(model_name)}_gid"
            instance_gid = kwargs[gid_field_name]
            instance_model_name, instance_id = from_global_id(instance_gid)
            if instance_model_name != f"{model_name}Node":
                raise UnauthorizedAccess("UnauthorizedAccess")
            instance = info.context["session"].query(model).get(instance_id)
            if not instance:
                raise UnauthorizedAccess("UnauthorizedAccess")
            kwargs["instance"] = instance
            if not authorize(instance, kwargs["current_user"]):
                raise UnauthorizedAccess("UnauthorizedAccess")
            return func(root, info, *args, **kwargs)
        return wrapper
    return decorator

この authorize_required デコレーターを使って、ユーザーが権限を持っている場合のみ、特定のアクションを実行できるようにします。

結論

GraphQL での認証と認可は、単一のエンドポイントで複雑に見えますが、適切に実装すれば非常に柔軟で強力なシステムを作成できます。本記事では、基本的なサインアップ、サインイン、認証、認可の仕組みを Python で実装する方法を紹介しました。この実装は、どんな GraphQL プロジェクトにも応用可能です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?