본문 바로가기

백앤드 프로그래밍/GraphQL

[GraphQL] 3. GraphQL에서의 인증 // Authentication in GraphQL

1. GraphQL에서의 인증 방식

GraphQL은 플랫폼 독립적이고 기존의 REST API와는 완전히 다르기 때문에 GraphQL에서의 인증은 큰 문제라고 생각할수도 있지만 인증은 그렇게 어렵지 않다. 기존의 JWT(Json Web Token)을 충분히 이용 가능하다

JWT 토큰을 전달하는 위치가 헤더에서 쿼리나 뮤테이션의 인자로 바뀌었을 뿐이다. 우리가 GraphQL 서버를 만들떄 사용하는 Flask와 Graphene에서 인증을 처리하는 것도 기존 Flask에서 Flask-JWT-Extended나 Flask-JWT-Simple을 사용했던 것처럼 Flask-GraphQL-Auth를 사용하면 그저 쿼리 리솔버나 뮤테이션에 인증 데코레이터를 적용해 우리가 익숙하게 사용하던 JWT(Json Web Token)를 사용해 인증을 간편하게 처리할 수 있다.

물론 graphene과 Flask로 만든 GraphQL 서버를 HTTP에서만 동작시킨다면 Flask의 리퀘스트 객체를 그대로 활용할 수 있기 때문에 헤더로 토큰을 전달하여 인증을 처리하는것이 가능하다. 그러나 이것이 GraphQL 한 방법이 아니라는것을 염두하자.

또한 JWT 토큰을 발급하고 리프레시하는 방식을 기존의 엔드포인트 방식 인증과 리프레시 뮤테이션을 만드는 방식으로 변경하면 기존의 인증 시스템을 GraphQL에 맞추어 가져올 수 있다.

2. GraphQL에서의 인증 시스템 구현

Flask-GraphQL-Auth, Graphene, Flask-GraphQL을 사용하여 인증 시스템을 구현해보자. 

먼저 JWT 토큰을 발급하기 위한 뮤테이션인 AuthMutation을 만들어보자. 이 뮤테이션은 액세스 토큰과 리프레시 토큰을 발급하며 코드는 다음과 같다. 


class AuthMutation(graphene.Mutation):
class Arguments(object):
username = graphene.String()
password = graphene.String()

access_token = graphene.String()
refresh_token = graphene.String()

def mutate(self, info, username, password):

return AuthMutation(access_token=create_access_token(username),
refresh_token=create_refresh_token(username))


두번째는 액세스 토큰이 만료되었을 때 리프레시 토큰을 사용해 엑세스 토큰을 발급하는 뮤테이션인 RefreshMutation이다. 이 뮤테이션은 리프레시 토큰을 검증하는 jwt_refresh_token_required 데코레이터를 사용하여 받은 jwt identity를 이용해 새 엑세스 토큰을 만드며 코드는 다음과 같다.


class RefreshMutation(graphene.Mutation):
class Arguments(object):
token = graphene.String()

new_token = graphene.String()

@jwt_refresh_token_required
def mutate(self, info):
current_user = get_jwt_identity()
return RefreshMutation(new_token=create_access_token(identity=current_user))


마지막으로 인증이 필요한 쿼리를 하나 만들어 보자. 앞에서 만든 뮤테이션들이 만든 토큰을 시험하기 위해서다. 이 쿼리는 받은 토큰의 정보를 제공한다.


class Query(graphene.ObjectType):
protected = graphene.String(token=graphene.String())

@jwt_required
def resolve_protected(self, info, message):
return return str(get_raw_jwt())


다음은 전체 코드이다. 한번 테스트 해보자


from flask import Flask
import graphene
from flask_graphql_auth import *
from flask_graphql import GraphQLView

app = Flask(__name__)
auth = GraphQLAuth(app)

app.config["JWT_SECRET_KEY"] = "something" # change this!
app.config["REFRESH_EXP_LENGTH"] = 30
app.config["ACCESS_EXP_LENGTH"] = 10


class AuthMutation(graphene.Mutation):
class Arguments(object):
username = graphene.String()
password = graphene.String()

access_token = graphene.String()
refresh_token = graphene.String()

def mutate(self, info, username, password):

return AuthMutation(access_token=create_access_token(username),
refresh_token=create_refresh_token(username))


class RefreshMutation(graphene.Mutation):
class Arguments(object):
token = graphene.String()

new_token = graphene.String()

@jwt_refresh_token_required
def mutate(self, info):
current_user = get_jwt_identity()
return RefreshMutation(new_token=create_access_token(identity=current_user))


class Mutation(graphene.ObjectType):
auth = AuthMutation.Field()
refresh = RefreshMutation.Field()
protected = ProtectedMutation.Field()


class Query(graphene.ObjectType):
protected = graphene.String(token=graphene.String())

@jwt_required
def resolve_protected(self, info, message):
return return str(get_raw_jwt())


schema = graphene.Schema(query=Query, mutation=Mutation)


app.add_url_rule(
'/graphql',
view_func=GraphQLView.as_view('graphql', schema=schema, graphiql=True)
)


if __name__ == '__main__':
app.run(debug=True)

3. Flask-GraphQL-Auth의 구현

마지막으로 Flask-GraphQL-Auth의 작동 방식을 알아보자.

def jwt_required(fn):
"""
A decorator to protect a resolver and mutation.


If you decorate an resolver or mutation with this, it will ensure that the requester
has a valid access token before allowing the resolver or mutation to be called. This
does not check the freshness of the access token.
"""
@wraps(fn)
def wrapper(*args, **kwargs):
token = kwargs.pop(current_app.config['JWT_TOKEN_ARGUMENT_NAME']) # token을 인자에서
pop해서 가져온다
try:
verify_jwt_in_argument(token) # 토큰 검증 및 ctx_stack에 유저 정보 등록
except jwt.ExpiredSignatureError:
return GraphQLError(str(RevokedTokenError()))
except Exception as e:
return GraphQLError(str(e))


return fn(*args, **kwargs)
return wrapper


GraphQL 서버가 요청을 받으면 클라이언트의 요청에 따라 해당하는 쿼리 리솔버와 뮤테이션을 실행시킨다. 여기서 인자는 kwargs로 받을 수 있기 때문에 쿼리 리솔버와 뮤테이션에 데코레이터를 사용해 이 인자들을 사용할 수 있다.  

Flask-GraphQL-Auth의 jwt_required나 jwt_refresh_token_required 데코레이터는 이를 이용해 쿼리 리솔버와 뮤테이션이 실행되기 전에 kwargs로 인자를 받아 유효한 요청인지 검사하는 일종의 미들웨어로 동작한다.   

요청이 유효하다면 context에 요청한 사용자를 등록하여 쿼리 리솔버나 뮤테이션이 사용할 수 있도록 하고 쿼리 리솔버나 뮤테이션은 원본 토큰을 넘겨받지 않게 한다.  

따라서 쿼리 리솔버나 뮤테이션은 기존의 Flask에서 사용했던 것처럼 get_raw_jwt, get_jwt_identity와 같은 컨텍스트 메소드를 사용해 요청의 주체를 식별할 수 있게 된다.

다만 현재 Flask-GraphQL-Auth에서 에러가 발생할 시 나오는 GraphQLLocatedError가 Flask의 errorhandler로 핸들링이 되지 않는 문제가 있어 토큰에 문제가 있으면 exception이 그대로 노출되는 문제가 있다. Flask-GraphQL-Auth는 이 에러 핸들링 문제만 해결이 된다면 1.0 릴리즈를 할 예정이다. 

사용자 클레임을 비롯해 좀 더 Flask-GraphQL-Auth에 대해 알아보고 싶다면 온라인 도큐멘테이션을 찾아보자. 


PS) 혹시 해결할 수 있는 아이디어가 있으신 분이나 기여하고 싶은 분들은 이슈나 PR을 부탁드립니다!