커스텀 토큰 만들기

Firebase는 보안 JSON 웹 토큰(JWT)으로 사용자나 기기 인증이 가능해 전체 인증 과정을 철저히 제어할 수 있습니다. 토큰을 서버에서 생성하고 클라이언트 기기에 전달한 다음 signInWithCustomToken() 메서드에서 이 토큰을 사용하여 인증하면 됩니다.

이를 위해서는 사용자 이름과 비밀번호 등 로그인 사용자 인증 정보를 수신하는 서버 엔드포인트를 만들어야 합니다. 서버 엔드포인트는 사용자 인증 정보가 올바르면 커스텀 JWT를 반환합니다. 클라이언트 기기는 서버에서 반환된 커스텀 JWT를 사용하여 Firebase(iOS+, Android, )에 인증할 수 있습니다. 인증된 ID는 Firebase 실시간 데이터베이스, Cloud Storage 등의 다른 Firebase 서비스에 액세스하는 데 사용됩니다. 실시간 데이터베이스 보안 규칙auth 객체 및 Cloud Storage 보안 규칙request.auth 객체에서 JWT의 내용을 확인할 수 있습니다.

Firebase Admin SDK로 커스텀 토큰을 만들거나, Firebase가 지원하는 않는 언어로 서버가 작성된 경우에는 서드 파티 JWT 라이브러리를 사용할 수도 있습니다.

시작하기 전에

커스텀 토큰은 서명에 사용되는 비공개 키가 Google 서비스 계정에 속해 있는 서명된 JWT입니다. Firebase Admin SDK가 커스텀 토큰에 서명하는 데 특정 Google 서비스 계정을 사용하도록 지정하는 방법은 다음과 같습니다.

  • 서비스 계정 JSON 파일 사용 -- 이 방법은 어느 환경에서나 사용할 수 있지만 반드시 서비스 계정 JSON 파일을 코드와 함께 패키징해야 합니다. 서비스 계정 JSON 파일이 외부에 공개되지 않도록 각별히 주의해야 합니다.
  • Admin SDK의 서비스 계정 탐색 허용 -- 이 방법은 Google Cloud Functions 및 App Engine과 같은 Google에서 관리하는 환경에서 사용할 수 있습니다. Google Cloud 콘솔을 통해 일부 권한을 추가적으로 구성해야 할 수 있습니다.
  • 서비스 계정 ID 사용 -- 이 방법은 Google이 관리하는 환경에서 사용할 경우 지정된 서비스 계정의 키로 토큰에 서명하지만 원격 웹 서비스를 사용하며, 사용자가 Google Cloud 콘솔을 통해 이 서비스 계정의 권한을 추가적으로 구성해야 할 수 있습니다.

서비스 계정 JSON 파일 사용

서비스 계정 JSON 파일에는 서비스 계정과 관련된 모든 정보가 포함되어 있으며(RSA 비공개 키 포함) Firebase Console에서 다운로드할 수 있습니다. 서비스 계정 JSON 파일을 통해 Admin SDK를 초기화하는 방법은 Admin SDK 설정 안내를 참조하세요.

이 초기화 방법은 광범위한 Admin SDK 배포에 적합합니다. 또한, 이 방법을 사용하면 Admin SDK로 원격 API 호출 없이 로컬에서 커스텀 토큰을 만들고 서명할 수 있습니다. 이 접근법의 큰 단점은 서비스 계정 JSON 파일과 코드를 함께 패키징해야 한다는 점입니다. 또한, 서비스 계정 JSON 파일의 비공개 키는 민감한 정보이므로 기밀 유지를 위해 각별히 주의해야 합니다. 특히, 서비스 계정 JSON 파일을 공개 버전 제어에 추가하지 말아야 합니다.

Admin SDK의 서비스 계정 탐색 허용

코드가 Google에서 관리하는 환경에 배포되면 Admin SDK가 커스텀 토큰에 서명할 수단을 자동으로 탐색하려고 시도합니다.

  • 코드가 Java, Python 또는 Go용 Google App Engine 표준 환경에서 배포되는 경우 Admin SDK는 해당 환경에 있는 앱 ID 서비스를 사용하여 커스텀 토큰에 서명할 수 있습니다. 앱 ID 서비스는 Google App Engine을 통해 앱에 프로비저닝된 서비스 계정을 사용하여 데이터에 서명합니다.

  • 코드가 기타 관리형 환경에서 배포되는 경우(예: Google Cloud Functions, Google Compute Engine) Firebase Admin SDK가 로컬 메타데이터 서버에서 서비스 계정 ID 문자열을 자동으로 탐색할 수 있습니다. 탐색된 서비스 계정 ID는 이후 IAM 서비스와 함께 원격으로 토큰을 서명하는 데 사용됩니다.

이 서명 방식을 사용하려면 Google 애플리케이션 기본 사용자 인증 정보를 통해 SDK를 초기화하고, 서비스 계정 ID 문자열은 지정하지 마세요.

Node.js

initializeApp();

Java

FirebaseApp.initializeApp();

Python

default_app = firebase_admin.initialize_app()

Go

app, err := firebase.NewApp(context.Background(), nil)
if err != nil {
	log.Fatalf("error initializing app: %v\n", err)
}

C#

FirebaseApp.Create();

로컬에서 동일한 코드를 테스트하려면 서비스 계정 JSON 파일을 다운로드하고 GOOGLE_APPLICATION_CREDENTIALS 환경 변수가 이 파일을 가리키도록 설정합니다.

Firebase Admin SDK가 서비스 계정 ID 문자열을 탐색해야 하는 경우 코드가 처음으로 커스텀 토큰을 만들 때 이를 수행합니다. 결과가 캐시되며 이후의 토큰 서명 작업에 다시 사용됩니다. 보통 자동 탐색된 서비스 계정 ID는 Google Cloud에서 제공하는 기본 서비스 계정 중 하나입니다.

명시적으로 지정된 서비스 계정 ID와 마찬가지로 자동 탐색된 서비스 계정 ID에서 커스텀 토큰 만들기가 작동하려면 iam.serviceAccounts.signBlob 권한이 필요합니다. 기본 서비스 계정에 필수 권한을 부여하기 위해 Google Cloud 콘솔의 IAM 및 관리 섹션을 사용해야 할 수 있습니다. 자세한 내용은 아래의 문제 해결 섹션을 참조하세요.

서비스 계정 ID 사용

애플리케이션의 다양한 부분에서 일관성을 유지하기 위해 서비스 계정 ID를 지정할 수 있으며, Google이 관리하는 환경에서 실행할 때 이 ID의 키가 토큰 서명에 사용됩니다. 이렇게 하면 IAM 정책을 보다 간단하고 안전하게 만들 수 있으며 코드에 서비스 계정 JSON 파일을 포함할 필요가 없습니다.

서비스 계정 ID는 Google Cloud 콘솔 또는 다운로드한 서비스 계정 JSON 파일의 client_email 필드에서 확인할 수 있습니다. 서비스 계정 ID는 <client-id>@<project-id>.iam.gserviceaccount.com 형식의 이메일 주소이며 Firebase 및 Google Cloud 프로젝트에서 고유하게 서비스 계정을 식별합니다.

별도의 서비스 계정 ID를 사용하여 커스텀 토큰을 만들려면 아래와 같이 SDK를 초기화합니다.

Node.js

initializeApp({
  serviceAccountId: 'my-client-id@my-project-id.iam.gserviceaccount.com',
});

Java

FirebaseOptions options = FirebaseOptions.builder()
    .setCredentials(GoogleCredentials.getApplicationDefault())
    .setServiceAccountId("my-client-id@my-project-id.iam.gserviceaccount.com")
    .build();
FirebaseApp.initializeApp(options);

Python

options = {
    'serviceAccountId': 'my-client-id@my-project-id.iam.gserviceaccount.com',
}
firebase_admin.initialize_app(options=options)

Go

conf := &firebase.Config{
	ServiceAccountID: "my-client-id@my-project-id.iam.gserviceaccount.com",
}
app, err := firebase.NewApp(context.Background(), conf)
if err != nil {
	log.Fatalf("error initializing app: %v\n", err)
}

C#

FirebaseApp.Create(new AppOptions()
{
    Credential = GoogleCredential.GetApplicationDefault(),
    ServiceAccountId = "my-client-id@my-project-id.iam.gserviceaccount.com",
});

서비스 계정 ID는 민감한 정보가 아니므로 노출이 되어도 괜찮습니다. 하지만 지정된 서비스 계정으로 커스텀 토큰에 서명하려면 Firebase Admin SDK가 원격 서비스를 호출해야 합니다. 또한 Admin SDK가 이 호출에 사용하는 서비스 계정(대개 {project-name}@appspot.gserviceaccount.com)에 iam.serviceAccounts.signBlob 권한이 있어야 합니다. 자세한 내용은 아래의 문제 해결 섹션을 참조하세요.

Firebase Admin SDK를 사용하여 커스텀 토큰 만들기

Firebase Admin SDK에는 커스텀 토큰을 만드는 메서드가 내장되어 있습니다. 가장 간단한 방법은 인증하는 사용자 또는 기기를 고유하게 식별하는 임의의 문자열인 uid를 제공하는 것입니다. 이러한 토큰은 1시간 후에 만료됩니다.

Node.js

const uid = 'some-uid';

getAuth()
  .createCustomToken(uid)
  .then((customToken) => {
    // Send token back to client
  })
  .catch((error) => {
    console.log('Error creating custom token:', error);
  });

Java

String uid = "some-uid";

String customToken = FirebaseAuth.getInstance().createCustomToken(uid);
// Send token back to client

Python

uid = 'some-uid'

custom_token = auth.create_custom_token(uid)

Go

client, err := app.Auth(context.Background())
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}

token, err := client.CustomToken(ctx, "some-uid")
if err != nil {
	log.Fatalf("error minting custom token: %v\n", err)
}

log.Printf("Got custom token: %v\n", token)

C#

var uid = "some-uid";

string customToken = await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync(uid);
// Send token back to client

필요한 경우 커스텀 토큰에 포함할 추가 클레임을 지정할 수도 있습니다. 예를 들어 아래에서는 커스텀 토큰에 premiumAccount 필드를 추가했으며, 보안 규칙의 auth / request.auth 객체에서 이 필드를 확인할 수 있습니다.

Node.js

const userId = 'some-uid';
const additionalClaims = {
  premiumAccount: true,
};

getAuth()
  .createCustomToken(userId, additionalClaims)
  .then((customToken) => {
    // Send token back to client
  })
  .catch((error) => {
    console.log('Error creating custom token:', error);
  });

Java

String uid = "some-uid";
Map<String, Object> additionalClaims = new HashMap<String, Object>();
additionalClaims.put("premiumAccount", true);

String customToken = FirebaseAuth.getInstance()
    .createCustomToken(uid, additionalClaims);
// Send token back to client

Python

uid = 'some-uid'
additional_claims = {
    'premiumAccount': True
}

custom_token = auth.create_custom_token(uid, additional_claims)

Go

client, err := app.Auth(context.Background())
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}

claims := map[string]interface{}{
	"premiumAccount": true,
}

token, err := client.CustomTokenWithClaims(ctx, "some-uid", claims)
if err != nil {
	log.Fatalf("error minting custom token: %v\n", err)
}

log.Printf("Got custom token: %v\n", token)

C#

var uid = "some-uid";
var additionalClaims = new Dictionary<string, object>()
{
    { "premiumAccount", true },
};

string customToken = await FirebaseAuth.DefaultInstance
    .CreateCustomTokenAsync(uid, additionalClaims);
// Send token back to client

예약된 커스텀 토큰 이름

클라이언트에서 커스텀 토큰으로 로그인

커스텀 토큰을 만들었으면 클라이언트 앱으로 보내야 합니다. 클라이언트 앱은 다음과 같이 signInWithCustomToken()을 호출하여 커스텀 토큰으로 인증을 진행합니다.

iOS+

Objective-C
[[FIRAuth auth] signInWithCustomToken:customToken
                           completion:^(FIRAuthDataResult * _Nullable authResult,
                                        NSError * _Nullable error) {
  // ...
}];
Swift
Auth.auth().signIn(withCustomToken: customToken ?? "") { user, error in
  // ...
}

Android

mAuth.signInWithCustomToken(mCustomToken)
        .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
            @Override
            public void onComplete(@NonNull Task<AuthResult> task) {
                if (task.isSuccessful()) {
                    // Sign in success, update UI with the signed-in user's information
                    Log.d(TAG, "signInWithCustomToken:success");
                    FirebaseUser user = mAuth.getCurrentUser();
                    updateUI(user);
                } else {
                    // If sign in fails, display a message to the user.
                    Log.w(TAG, "signInWithCustomToken:failure", task.getException());
                    Toast.makeText(CustomAuthActivity.this, "Authentication failed.",
                            Toast.LENGTH_SHORT).show();
                    updateUI(null);
                }
            }
        });

Unity

auth.SignInWithCustomTokenAsync(custom_token).ContinueWith(task => {
  if (task.IsCanceled) {
    Debug.LogError("SignInWithCustomTokenAsync was canceled.");
    return;
  }
  if (task.IsFaulted) {
    Debug.LogError("SignInWithCustomTokenAsync encountered an error: " + task.Exception);
    return;
  }

  Firebase.Auth.AuthResult result = task.Result;
  Debug.LogFormat("User signed in successfully: {0} ({1})",
      result.User.DisplayName, result.User.UserId);
});

C++

firebase::Future<firebase::auth::AuthResult> result =
    auth->SignInWithCustomToken(custom_token);

웹 네임스페이스화된 API

firebase.auth().signInWithCustomToken(token)
  .then((userCredential) => {
    // Signed in
    var user = userCredential.user;
    // ...
  })
  .catch((error) => {
    var errorCode = error.code;
    var errorMessage = error.message;
    // ...
  });

웹 모듈식 API

import { getAuth, signInWithCustomToken } from "firebase/auth";

const auth = getAuth();
signInWithCustomToken(auth, token)
  .then((userCredential) => {
    // Signed in
    const user = userCredential.user;
    // ...
  })
  .catch((error) => {
    const errorCode = error.code;
    const errorMessage = error.message;
    // ...
  });

인증에 성공하면 사용자가 커스텀 토큰에 포함된 uid에 지정된 계정으로 클라이언트 앱에 로그인하게 됩니다. 지금까지 없었던 계정이라면 해당 사용자의 레코드가 생성됩니다.

실시간 데이터베이스 보안 규칙auth 객체 및 Cloud Storage 보안 규칙request.auth 객체는 다른 로그인 메서드(signInWithEmailAndPassword()signInWithCredential())와 마찬가지로 사용자의 uid로 채워집니다. 이 경우 uid는 커스텀 토큰을 생성할 때 지정한 항목입니다.

데이터베이스 규칙

{
  "rules": {
    "adminContent": {
      ".read": "auth.uid === 'some-uid'"
    }
  }
}

스토리지 규칙

service firebase.storage {
  match /b/<your-firebase-storage-bucket>/o {
    match /adminContent/{filename} {
      allow read, write: if request.auth != null && request.auth.uid == "some-uid";
    }
  }
}

커스텀 토큰에 추가 클레임이 포함된 경우 규칙의 auth.token(Firebase 실시간 데이터베이스) 또는 request.auth.token(Cloud Storage) 객체에서 이러한 클레임을 참조할 수 있습니다.

데이터베이스 규칙

{
  "rules": {
    "premiumContent": {
      ".read": "auth.token.premiumAccount === true"
    }
  }
}

스토리지 규칙

service firebase.storage {
  match /b/<your-firebase-storage-bucket>/o {
    match /premiumContent/{filename} {
      allow read, write: if request.auth.token.premiumAccount == true;
    }
  }
}

서드 파티 JWT 라이브러리로 커스텀 토큰 만들기

백엔드가 공식 Firebase Admin SDK가 없는 언어로 작성되었더라도 커스텀 토큰을 직접 만들 수 있습니다. 우선 해당 언어의 서드 파티 JWT 라이브러리를 검색합니다. 그런 다음 이 JWT 라이브러리를 사용하여 다음과 같은 클레임을 포함하는 JWT를 발행합니다.

커스텀 토큰 클레임
alg 알고리즘 "RS256"
iss 발급자 프로젝트의 서비스 계정 이메일 주소
sub 제목 프로젝트의 서비스 계정 이메일 주소
aud 잠재고객 "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"
iat 발급 시간 Unix epoch를 기준으로 하는 현재 시간(초)
exp 만료 시간 Unix epoch를 기준으로 하는 토큰 만료 시간(초). iat보다 최대 3,600초 길어질 수 있습니다.
참고: 이 항목은 커스텀 토큰 자체의 만료 시간만 제어합니다. signInWithCustomToken()으로 사용자가 로그인한 후에는 세션이 무효화되거나 사용자가 로그아웃할 때까지 기기에서 로그인 상태가 유지됩니다.
uid 로그인한 사용자의 고유 식별자는 1~128자(영문 기준) 사이의 문자열이어야 합니다. uid가 짧을수록 성능이 향상됩니다.
claims(선택사항) 보안 규칙의 auth / request.auth 변수에 포함할 선택적인 커스텀 클레임

다음은 Firebase Admin SDK가 지원되지 않는 몇 가지 언어에서 커스텀 토큰의 생성 방법을 구현한 예시입니다.

PHP

php-jwt 사용:

// Requires: composer require firebase/php-jwt
use Firebase\JWT\JWT;

// Get your service account's email address and private key from the JSON key file
$service_account_email = "abc-123@a-b-c-123.iam.gserviceaccount.com";
$private_key = "-----BEGIN PRIVATE KEY-----...";

function create_custom_token($uid, $is_premium_account) {
  global $service_account_email, $private_key;

  $now_seconds = time();
  $payload = array(
    "iss" => $service_account_email,
    "sub" => $service_account_email,
    "aud" => "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
    "iat" => $now_seconds,
    "exp" => $now_seconds+(60*60),  // Maximum expiration time is one hour
    "uid" => $uid,
    "claims" => array(
      "premium_account" => $is_premium_account
    )
  );
  return JWT::encode($payload, $private_key, "RS256");
}

Ruby

ruby-jwt 사용:

require "jwt"

# Get your service account's email address and private key from the JSON key file
$service_account_email = "service-account@my-project-abc123.iam.gserviceaccount.com"
$private_key = OpenSSL::PKey::RSA.new "-----BEGIN PRIVATE KEY-----\n..."

def create_custom_token(uid, is_premium_account)
  now_seconds = Time.now.to_i
  payload = {:iss => $service_account_email,
             :sub => $service_account_email,
             :aud => "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
             :iat => now_seconds,
             :exp => now_seconds+(60*60), # Maximum expiration time is one hour
             :uid => uid,
             :claims => {:premium_account => is_premium_account}}
  JWT.encode payload, $private_key, "RS256"
end

커스텀 토큰을 생성한 후에는 이 토큰을 Firebase 인증에 사용할 수 있도록 클라이언트 앱으로 전송합니다. 방법은 위 코드 샘플을 참조하세요.

문제 해결

이 섹션에서는 개발자가 커스텀 토큰을 만들 때 발생할 수 있는 일반적인 문제와 이에 대한 해결 방법을 간략하게 다룹니다.

IAM API가 사용 설정되지 않음

토큰 서명을 위해 서비스 계정 ID를 지정하는 경우 다음과 유사한 오류가 발생할 수 있습니다.

Identity and Access Management (IAM) API has not been used in project
1234567890 before or it is disabled. Enable it by visiting
https://console.developers.google.com/apis/api/iam.googleapis.com/overview?project=1234567890
then retry. If you enabled this API recently, wait a few minutes for the action
to propagate to our systems and retry.

Firebase Admin SDK가 IAM API를 사용하여 토큰에 서명합니다. 이 오류는 IAM API가 현재 Firebase 프로젝트에 대해 사용 설정되지 않았음을 나타냅니다. 웹브라우저의 오류 메시지에서 링크를 열고 'API 사용 설정' 버튼을 클릭하여 프로젝트에 대해 사용 설정합니다.

서비스 계정에 필수 권한이 없음

Firebase Admin SDK를 실행하는 서비스 계정에 iam.serviceAccounts.signBlob 권한이 없는 경우 다음과 같은 오류 메시지가 표시될 수 있습니다.

Permission iam.serviceAccounts.signBlob is required to perform this operation
on service account projects/-/serviceAccounts/{your-service-account-id}.

이 문제를 해결하는 가장 간단한 방법은 '서비스 계정 토큰 생성자' IAM 역할을 문제의 서비스 계정(대개 {project-name}@appspot.gserviceaccount.com)에 부여하는 것입니다.

  1. Google Cloud 콘솔에서 IAM 및 관리 페이지를 엽니다.
  2. 프로젝트를 선택하고 '계속'을 클릭합니다.
  3. 업데이트할 서비스 계정에 해당하는 수정 아이콘을 클릭합니다.
  4. '다른 역할 추가'를 클릭합니다.
  5. 검색 필터에 '서비스 계정 토큰 생성자'를 입력하고 결과에서 이를 선택합니다.
  6. '저장'을 클릭하여 역할 부여를 확인합니다.

이 프로세스에 대한 자세한 내용을 보거나 gcloud 명령줄 도구를 사용하여 역할을 업데이트하는 방법을 보려면 IAM 문서를 참조하세요.

서비스 계정 확인 실패

다음과 유사한 오류 메시지가 발생할 경우 Firebase Admin SDK가 올바르게 초기화되지 않은 것입니다.

Failed to determine service account ID. Initialize the SDK with service account
credentials or specify a service account ID with iam.serviceAccounts.signBlob
permission.

SDK를 통해 서비스 계정 ID를 자동 탐색하는 경우 코드가 메타데이터 서버를 통해 관리형 Google 환경에 배포되는지 확인하세요. 그렇지 않은 경우 SDK 초기화 시 서비스 계정 JSON 파일 또는 서비스 계정 ID를 지정하세요.