Hasura + Firebase Auth (JWT)

Hasura + Firebase Auth (JWT)

본 포스트는 DB & Auth 개발을 하며 삽질한 내용을 다룹니다

계기

필자는 혼자서 서비스를 개발하고 있었습니다. 개발자 관점에서는 본 서비스는 다음과 같이 간단합니다.

사용자는 로그인을 하고, 돈을 낸 만큼 데이터를 열람할 수 있습니다.

저는 serverless로 scalable하고 보안적으로 문제가 없게 구현하고 싶었습니다.

백엔드를 만들고 운영하면서 계정관리도하고 데이터베이스 접근도 다 검사하는 무식한 방법은 미래를 위해 좋지 않습니다.

기연

필자의 친구 중에는 고수가 있습니다. 고수에게 조언을 구했습니다.

고수가 말하길

내가 다 써보니, Auth0 + Hasura + (Postgres / Docker) 를 추천한다.

손쉽게 지름길에 도달해서 공부를 시작했습니다.

사실 원래는 모든 Backend를 Firebase 로 다 구현하려고 했습니다.
왜냐하면, Frontend 를 Flutter 로 구현하고 있고, 같은 구글의 서비스가 연동이 잘 되어있기 때문이죠. 쉽게 사용할 수 있는 패키지나 문서가 많습니다.
Firebase 에는 Hosting도 Database도 Auth도 Storage도 다 구비되어 있습니다. 서비스를 런칭하면 하나의 계정으로 한곳에서 모니터링, 결제 등등 관리가 편하죠.

구현을 하려다보니 Database (Firestore) 에서 rules를 설정해서 접근 제한을 걸어야하는데, 이게 스크립트로 짜야하는 것이 제가 원하는 기능들을 세세하게 구현하기가 어려웠습니다. 결국 Cloud Function의 도움을 받아야할 것 같더군요.

지름길로 가 봅시다.

Hasura + Neon

Hasura 라는 것을 처음 들어봤는데, 이러저러한 데이터베이스들 위에 얹어서 API도 만들고, 접근제어도 하고, 모니터링도 하고 이것저것 할 수 있는 서비스 입니다. 제가 원하는 것들을 직접 구현하지 않아도 해주는 녀석이죠.

그럼 데이터베이스를 골라야합니다.

고수의 추천은 Docker에 Postgres를 올리고, 나중에 사용자가 많아지면 k8s로 늘리면 된다는 것 이었습니다. 그런데, Hasura 공홈 안내에는 두가지 방법을 추천하더군요.

  1. 위와 같은 방법. Docker + Postgres 를 self로 올려 연결합니다.
  2. Neon serverless postgres 를 사용하는 방법
    (이 외에도 엄청 다양한 database를 연결하는 방법들이 잘 안내되어 있습니다.)

Neon이라는 서비스를 또 처음 들어봤지만, 제가 원하는 그것을 돈내면 해주는 곳 입니다. 참 좋은 서비스들이 많습니다.

그래서 귀찮으니 Neon을 골랐습니다. 연동도 거의 원클릭으로 됩니다. 가격비교를 해보지는 않았지만 같은 요청/저장 을 하면 더 비싸겠죠. 나중에 서비스가 잘 운영되고 원가를 절감하고 싶으면 그때가서 바꿔끼면 되겠습니다.

Auth

Hasura는 여러가지의 Auth 서비스들을 함께 사용할 수 있었습니다.

Auth0, firebase auth, clerk 등등 10가지 넘게 문서화가 잘 되어있었습니다.

보니까 제가 사용하고자하는 frontend 에 auth0 연동하는 것이 문서화가 잘 안되어있더군요. 그래서, 고수에게는 미안하지만 그냥 firebase auth를 골랐습니다.

이제부터 삽질 시작입니다.

JWT, Custom Claim

이 글을 미래의 저를 위한 글이기도 하기 때문에 찾아봐야할 문서들을 남깁니다.
키워드로 검색해서 오신 고수분들은 이 링크들만 보셔도 될 겁니다.

  1. Hasura Auth (Authentication, Authorization) 문서 중 JWT 관련 내용
  2. Hasura + Firebase Auth Sample App

큰 구조는 아래와 같습니다.

Auth 서비스에서 JWT를 받아와서 Hasura에 API 요청을 할때 같이 넘겨줍니다.

해야할 일은 아래와 같습니다.

Hasura에서 auth 정보를 볼 수 있게 설정해줘야 합니다.

저는 firebase auth를 사용하니 이 정보를 hasura environment variable에 설정해줘야합니다. 아래 내용을 따라하시면 됩니다. 그냥 입력하면 되는 거라 뭐 딱히 설명하지는 않겠습니다.

Firebase | Hasura Authentication Tutorial
Firebase enables you to add authentication to your applications. Learn how to integrate Firebase with Hasura using JWT

Hasura에서 요하는 정보들을 JWT에 넣어줘야합니다.

JWT 에 포함되어야 할 스펙들은 Hasura 문서에 잘 나와있습니다.
JWT 에 role 같은 것들이 다 들어있어야 합니다.

여기서 삽질을 많이 했는데요, 저는 개인적으로 Auth 관련 내용을 처음 구현 해봅니다.  JWT고, GraphQL이고 다 처음 구현하는 거라 어떤 식으로 보안이 안뚫리게 만드는 것인지 잘 몰랐습니다.

Firebase auth 에서 JWT에 내용들을 추가해줘야하는데, Firebase의 설명은 아래 문서를 참조하면됩니다.

Control Access with Custom Claims and Security Rules | Firebase Authentication

어찌되었건 Cloud Function 를 사용해야 한다는 것입니다.
이걸 안하려는 것도 이유 중에 하나였는 데, 결국 하게되었네요...

아래와 같은 코드를 올렸는데요, 내용은 간단합니다.
signup 할때마다 불리고, email 도메인따라 admin을 주거나, user 를 주거나 둘 중 하나입니다. user/admin은 hasura에서 데이터 접근 권한을 가를때 사용합니다.

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

// On sign up.
exports.processSignUp = functions.auth.user().onCreate(user => {
  console.log(user);
  // Check if user meets role criteria:
  // Your custom logic here: to decide what roles and other `x-hasura-*` should the user get
  let customClaims;
  if (user.email && user.email.indexOf('@hasura.io') !== -1) {
    customClaims = {
      'https://hasura.io/jwt/claims': {
        'x-hasura-default-role': 'admin',
        'x-hasura-allowed-roles': ['user', 'admin'],
        'x-hasura-user-id': user.uid
      }
    };
  }
  else {
    customClaims = {
      'https://hasura.io/jwt/claims': {
        'x-hasura-default-role': 'user',
        'x-hasura-allowed-roles': ['user'],
        'x-hasura-user-id': user.uid
      }
    };
  }
  // Set custom user claims on this newly created user.
  return admin.auth().setCustomUserClaims(user.uid, customClaims)
    .then(() => {
      // Update real-time database to notify client to force refresh.
      const metadataRef = admin.database().ref("metadata/" + user.uid);
      // Set the refresh time to the current UTC timestamp.
      // This will be captured on the client to force a token refresh.
      return metadataRef.set({refreshTime: new Date().getTime()});
    })
    .catch(error => {
      console.log(error);
    });
});

요런 함수를 올리고 실제로 jwt를 발행받아서 한번 열람해봤습니다.

jwt.io에 토큰을 붙여넣으면 볼 수 있습니다.

이렇게 Firebase auth 에서 받은 jwt 에 hasura 에서 필요한 내용들이 추가가 되었네요.

이제 저 function을 결제랑 연동해서 role들을 분기시켜줘야합니다. 이건 나중에 구현할 것이고요.

Hasura에 요청하기

API는 GraphQL 이나 REST API 를 사용가능한데, 둘 다 헤더에 아래와 같이 내용을 추가하면 Role에 맞게 데이터를 거절하거나, 일부만 보여주거나 설정된대로 알아서 해줍니다.

Authorization Bearer $JWT

잡소리

요렇게 하나 이해하고 구현하는데 하루가 더 걸렸네요 ;;

덕분에 JWT랑 Auth관련해서 많은 지식들을 얻었습니다.

JWT에 내용이 저렇게 쏙쏙 담기는 거라는 것을 처음 알았습니다.

이걸 하려면 firebase auth 에서는 cloud function으로 auth랑 연결시켜야 한다는 것이 참 예상 외 였네요. 정말 이렇게 해야하나? auth에서 뭔가 설정하면 될 줄 알았는데, 엄한데 찾느라고 시간 많이 날렸습니다