맞춤 토큰 만들기

Firebase는 보안 JSON 웹 토큰(JWT)을 통한 사용자 또는 기기 인증을 허용하므로 인증의 전 과정을 철저히 관리할 수 있습니다. 서버에서 이러한 토큰을 생성하고 클라이언트 기기에 다시 전달한 후 signInWithCustomToken() 메소드를 통해 토큰을 사용하여 인증할 수 있습니다.

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

Firebase Admin SDK로 맞춤 토큰을 만들 수도 있고, Firebase가 기본적으로 지원하는 않는 언어로 서버가 작성된 경우 타사 JWT 라이브러리를 사용할 수도 있습니다.

시작하기 전에

맞춤 토큰은 서명에 사용되는 비공개 키가 Google 서비스 계정에 속해 있는 서명된 JWT입니다. 다음과 같은 몇 가지 방법으로 Firebase Admin SDK가 Firebase Admin SDK 맞춤 토큰에 서명하는 데 사용해야 하는 Google 서비스 계정을 지정할 수 있습니다.

  • 서비스 계정 JSON 파일 사용 -- 이 방법은 어떠한 환경에서도 사용할 수 있지만 서비스 계정 JSON 파일을 코드와 함께 패키징해야 합니다. 서비스 계정 JSON 파일이 외부에 공개되지 않도록 특별히 주의해야 합니다.
  • 서비스 계정 ID 사용 -- 이 방법은 어떠한 환경에서도 사용할 수 있지만 전체 서비스 계정 JSON 파일을 사용하는 것보다 더욱 간편하고 안전한 편입니다. 하지만 원격 웹 서비스를 사용하여 토큰에 서명하기 때문에 Google Cloud Platform 콘솔을 통해 추가 권한을 구성해야 할 수 있습니다.
  • Admin SDK가 서비스 계정 탐색 -- 이 방법은 Google Cloud Functions 및 Google App Engine과 같은 Google에서 관리하는 환경에서 사용할 수 있습니다. Google Cloud Platform 콘솔을 통해 추가 권한을 구성해야 할 수 있습니다.

서비스 계정 JSON 파일 사용

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

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

서비스 계정 ID 사용

전체 서비스 계정 JSON 파일과 코드를 함께 패키징하는 대신 서비스 계정 ID 문자열만 가지고 Firebase Admin SDK를 초기화할 수 있습니다. 이는 Firebase 콘솔에서 찾을 수 있습니다. 또는, 다운로드한 서비스 계정 JSON 파일의 client_email 필드에서 찾을 수 있습니다. 서비스 계정 ID는 다음과 같은 형식을 가진 이메일 주소입니다. <client-id>@<project-id>.iam.gserviceaccount.com. Firebase 및 Google Cloud Platform 프로젝트에서 고유하게 서비스 계정을 식별합니다.

서비스 계정 ID만으로 맞춤 토큰을 만들려면 아래 표시된 것처럼 SDK를 초기화합니다.

Node.js

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

자바

FirebaseOptions options = new FirebaseOptions.Builder()
    .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는 민감한 정보가 아니므로 노출이 되어도 괜찮습니다. 하지만 서비스 계정 ID만으로 맞춤 토큰에 서명하려면 Firebase Admin SDK가 원격 서비스를 호출해야 합니다. 또한, 이 목적으로 선택하는 서비스 계정에 iam.serviceAccounts.signBlob 권한이 있는지도 확인해야 합니다. 자세한 내용은 아래의 문제해결 섹션을 참조하세요.

Admin SDK가 서비스 계정 탐색

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

  • 코드가 자바, 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

admin.initializeApp();

자바

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 Platform에 의해 제공되는 기본 서비스 계정 중 하나입니다.

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

Firebase Admin SDK를 사용하여 맞춤 토큰 만들기

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

Node.js

var uid = "some-uid";

admin.auth().createCustomToken(uid)
  .then(function(customToken) {
    // Send token back to client
  })
  .catch(function(error) {
    console.log("Error creating custom token:", error);
  });

자바

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

var uid = "some-uid";
var additionalClaims = {
  premiumAccount: true
};

admin.auth().createCustomToken(uid, additionalClaims)
  .then(function(customToken) {
    // Send token back to client
  })
  .catch(function(error) {
    console.log("Error creating custom token:", error);
  });

자바

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.FirebaseUser newUser = task.Result;
  Debug.LogFormat("User signed in successfully: {0} ({1})",
      newUser.DisplayName, newUser.UserId);
});

C++

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

firebase.auth().signInWithCustomToken(token).catch(function(error) {
  // Handle Errors here.
  var errorCode = error.code;
  var errorMessage = error.message;
  // ...
});

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

다른 로그인 메소드(예: signInWithEmailAndPassword()signInWithCredential())와 동일한 방식으로 Firebase 실시간 데이터베이스 보안 규칙auth 객체 및 Cloud Storage 보안 규칙request.auth 객체에 사용자의 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.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 기점을 기준으로 하는 현재 시간(초)
exp 만료 시간 UNIX 기점을 기준으로 하는 토큰 만료 시간(초). iat보다 최대 3600초까지 길어질 수 있습니다.
참고: 이 항목은 맞춤 토큰 자체의 만료 시간만 제어합니다. signInWithCustomToken()으로 사용자가 로그인한 후에는 세션이 무효화되거나 사용자가 로그아웃할 때까지 기기에서 로그인 상태가 유지됩니다.
uid 로그인한 사용자의 고유 식별자(1~36자의 길이의 문자열)
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 사용 설정' 버튼을 클릭하여 프로젝트에 대해 사용 설정합니다.

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

토큰에 서명하기 위해 사용되는 서비스 계정 ID에 iam.serviceAccounts.signBlob 권한이 없는 경우 다음과 같은 오류 메시지가 발생할 수 있습니다.

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

이를 해결하는 가장 간편한 방법은 '서비스 계정 토큰 생성자' IAM 역할을 문제가 되는 서비스 계정에 부여하는 것입니다.

  1. Google Cloud Platform 콘솔에서 IAM 및 관리 페이지를 엽니다.
  2. 프로젝트를 선택하고 '계속'을 클릭합니다.
  3. 업데이트할 서비스 계정 ID에 해당하는 편집 아이콘을 클릭합니다.
  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를 지정하세요.

다음에 대한 의견 보내기...

도움이 필요하시나요? 지원 페이지를 방문하세요.