建立自訂權杖

Firebase 可讓您使用安全的 JSON Web Token (JWT) 驗證使用者或裝置,完全掌控驗證程序。您可以在伺服器上產生這些權杖,將其傳回用戶端裝置,然後使用這些權杖透過 signInWithCustomToken() 方法進行驗證。

為此,您必須建立伺服器端點,用於接受登入憑證 (例如使用者名稱和密碼),並在憑證有效時傳回自訂 JWT。接著,用戶端裝置可以使用伺服器傳回的自訂 JWT,向 Firebase 進行驗證 (iOS+、Android、網頁)。通過驗證後,系統會使用這個身分存取其他 Firebase 服務,例如 Firebase Realtime Database 和 Cloud Storage。此外,JWT 的內容會顯示在 Realtime Database Security Rules 的 auth 物件和 Cloud Storage Security Rules 的 request.auth 物件中。

您可以使用 Firebase Admin SDK 建立自訂權杖,或在伺服器使用 Firebase 原生不支援的語言時,使用第三方 JWT 程式庫。

事前準備

自訂權杖是已簽署的 JWT,用於簽署的私密金鑰屬於 Google 服務帳戶。您可以透過多種方式指定 Google 服務帳戶,讓 Firebase Admin SDK 用於簽署自訂權杖:

  • 讓 Admin SDK 探索服務帳戶 -- 這個方法可用於 Google 管理的環境,例如 Google Cloud Functions 和 App Engine。您可能需要透過 Google Cloud 控制台設定一些額外權限。
  • 使用服務帳戶 ID:這個方法會使用指定服務帳戶的 Google 代管金鑰簽署權杖,因此您不必自行處理任何私密金鑰。這項服務使用遠端網路服務,您可能需要透過 Google Cloud 控制台,為這個服務帳戶設定額外權限。
  • 使用服務帳戶金鑰檔案:只有在程式碼在 Google 代管環境以外執行,且無法使用上述任一方法時,才使用這個方法。您必須建立、儲存及保護長期私密金鑰。

建議您採用前兩種方法,因為您不必下載或管理服務帳戶金鑰。

讓 Admin SDK 探索服務帳戶

如果程式碼部署在 Google 代管的環境中,Admin SDK 會嘗試自動尋找簽署自訂權杖的方法:

  • 如果您的程式碼部署在 Java、Python 或 Go 的 App Engine 標準環境中,Admin SDK 可以使用該環境中的 身分識別服務簽署自訂權杖。App Identity 服務會使用 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();

如要在本機測試相同的程式碼,而不下載服務帳戶金鑰,請使用 Google Cloud CLI 透過自己的使用者憑證登入:

gcloud auth application-default login

由於本機電腦上沒有中繼資料伺服器,Admin SDK 無法自動探索服務帳戶 ID。請改為在初始化 SDK 時指定服務帳戶 ID,並確認您的使用者帳戶在該服務帳戶上具備服務帳戶權杖建立者角色。

如果 Firebase Admin SDK 必須尋找服務帳戶 ID 字串,則會在程式碼首次建立自訂權杖時執行。結果會快取並重複用於後續的權杖簽署作業。自動探索到的服務帳戶 ID 通常是 Google Cloud 提供的預設服務帳戶之一:

與明確指定的服務帳戶 ID 一樣,自動探索到的服務帳戶 ID 必須具備 iam.serviceAccounts.signBlob 權限,才能建立自訂權杖。您可能需要使用 Google Cloud 控制台的「IAM and admin」(IAM 與管理員) 專區,將必要權限授予預設服務帳戶。詳情請參閱下方的疑難排解一節。

使用服務帳戶 ID

如要維持應用程式各部分的一致性,您可以指定服務帳戶 ID,並使用其 Google 代管金鑰簽署權杖。這可簡化 IAM 政策並提高安全性,且不必下載、儲存或部署服務帳戶金鑰。

您可以在 Google Cloud 控制台中找到服務帳戶 ID。服務帳戶 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 權限。詳情請參閱下方的疑難排解一節。

使用服務帳戶金鑰檔案

如果程式碼是在 Google Cloud 以外的環境中執行,且上述兩種方法都無法使用,您可以使用服務帳戶金鑰檔案初始化 Admin SDK。詳情請參閱 Admin SDK 設定操作說明。使用這個方法時,Admin SDK 會在本機使用檔案中的私密金鑰簽署自訂權杖,不會發出遠端 API 呼叫。

服務帳戶金鑰是長效憑證。如果金鑰外洩,任何取得金鑰的人都能以服務帳戶的身分執行操作,直到金鑰刪除為止。請勿將金鑰檔案提交至版本管控,並考慮在環境支援的情況下,使用 Workload Identity Federation 取代金鑰。另請注意,部分組織會使用組織政策限制,禁止建立服務帳戶金鑰,此時您必須使用其他方法。

使用 Firebase Admin SDK 建立自訂權杖

Firebase Admin SDK 內建建立自訂權杖的方法。您至少必須提供 uid,這可以是任何字串,但應能明確識別您要驗證的使用者或裝置。這些權杖會在一小時後失效。

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);

Web

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

Web

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 指定的帳戶登入用戶端應用程式。如果該帳戶先前不存在,系統會為該使用者建立記錄。

與其他登入方式 (例如 signInWithEmailAndPassword() 和 signInWithCredential()) 相同,Realtime Database Security Rules 中的 auth 物件和 Cloud Storage Security Rules 中的 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 != null && request.auth.uid == "some-uid";
    }
  }
}

如果自訂權杖包含其他宣告,您可以在規則中參照 auth.token (Firebase Realtime Database) 或 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 晚 3,600 秒。
注意:這只會控制自訂權杖本身的到期時間。不過,一旦您使用 signInWithCustomToken() 讓使用者登入,他們就會保持登入裝置的狀態,直到工作階段失效或使用者登出為止。
uid 已登入使用者的專屬 ID 必須是字串,長度介於 1 到 128 個字元 (含) 之間。uid 越短,成效越好。
claims (選填) 要納入安全性規則 auth/request.auth 變數的選用自訂宣告

以下舉例說明如何以 Firebase Admin SDK 不支援的多種語言建立自訂權杖。這些範例會使用服務帳戶私密金鑰在本機簽署權杖,因此適用服務帳戶金鑰檔案的安全指南。為避免管理私密金鑰,您可以呼叫 IAM Service Account Credentials API 的 signJwt 方法,讓 Google 遠端簽署權杖酬載。呼叫端需要服務帳戶的服務帳戶權杖建立者角色。

PHP

使用 php-jwt:

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

// Placeholder values. In production, load your service account's email
// address and private key from a secure secret store. Never hard-code
// private keys in source code.
$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-jwt:

require "jwt"

# Placeholder values. In production, load your service account's email
# address and private key from a secure secret store. Never hard-code
# private keys in source code.
$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 簽署權杖。這項錯誤表示 Firebase 專案目前未啟用 IAM API。在網路瀏覽器中開啟錯誤訊息中的連結,然後按一下「Enable API」(啟用 API) 按鈕,為專案啟用 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 角色授予適當的服務帳戶。預設服務帳戶取決於環境和 Cloud Functions 版本:

  • Cloud Functions (第 1 代):使用預設 App Engine 服務帳戶。
  • Cloud Functions (第 2 代):使用預設的 Compute Engine 服務帳戶。
  1. 在 Google Cloud 控制台中,前往「 IAM and Admin」(IAM 與管理)。

  2. 按一下要更新的服務帳戶對應的「編輯」圖示。

  3. 按一下「新增其他角色」。

  4. 在搜尋篩選器中輸入 "Service Account Token Creator",然後從結果中選取該項目。

  5. 按一下「儲存」,確認授予角色。

如要進一步瞭解這項程序,請參閱 IAM 說明文件,或瞭解如何使用 gcloud 指令列工具更新角色。

無法判斷服務帳戶

如果收到類似下方的錯誤訊息,表示 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 初始化時指定服務帳戶 ID,或在非 Google 環境中,使用服務帳戶金鑰檔案初始化 SDK。