יצירת אסימונים בהתאמה אישית

ב-Firebase אתם יכולים לאמת משתמשים או מכשירים באמצעות אסימוני אינטרנט מאובטחים מסוג JSON‏ (JWT), וכך אתם מקבלים שליטה מלאה על האימות. אתם יוצרים את האסימונים האלה בשרת, מעבירים אותם בחזרה למכשיר הלקוח ואז משתמשים בהם כדי לאמת באמצעות השיטה signInWithCustomToken().

כדי לעשות את זה, צריך ליצור נקודת קצה בשרת שמקבלת פרטי כניסה, כמו שם משתמש וסיסמה, ואם פרטי הכניסה תקינים, מחזירה אסימון JWT בהתאמה אישית. לאחר מכן, מכשיר לקוח יכול להשתמש ב-JWT המותאם אישית שהוחזר מהשרת כדי לבצע אימות באמצעות Firebase (iOS+‎,‏ Android, אינטרנט). אחרי האימות, הזהות הזו תשמש לגישה לשירותים אחרים של Firebase, כמו Firebase Realtime Database ו-Cloud Storage. בנוסף, התוכן של ה-JWT יהיה זמין באובייקט auth ב-Realtime Database Security Rules ובאובייקט request.auth ב-Cloud Storage Security Rules.

אתם יכולים ליצור אסימון בהתאמה אישית באמצעות SDK של Firebase לאדמינים, או להשתמש בספריית JWT של צד שלישי אם השרת שלכם כתוב בשפה ש-Firebase לא תומכת בה באופן מקורי.

לפני שמתחילים

אסימונים בהתאמה אישית הם אסימוני JWT חתומים, שבהם המפתח הפרטי שמשמש לחתימה שייך לחשבון שירות של Google. יש כמה דרכים לציין את חשבון השירות של Google שבו ה-SDK של Firebase לאדמינים צריך להשתמש כדי לחתום על טוקנים בהתאמה אישית:

  • שימוש בקובץ JSON של חשבון שירות – אפשר להשתמש בשיטה הזו בכל סביבה, אבל צריך לארוז קובץ JSON של חשבון שירות יחד עם הקוד. חשוב לוודא שקובץ ה-JSON של חשבון השירות לא נחשף לגורמים חיצוניים.
  • זיהוי של חשבון שירות באמצעות SDK לאדמינים – אפשר להשתמש בשיטה הזו בסביבות שמנוהלות על ידי Google, כמו Cloud Functions ו-App Engine. יכול להיות שתצטרכו להגדיר הרשאות נוספות במסוף Google Cloud.
  • שימוש במזהה של חשבון שירות – כשמשתמשים בשיטה הזו בסביבה שמנוהלת על ידי Google, היא חותמת על טוקנים באמצעות המפתח של חשבון השירות שצוין. עם זאת, הוא משתמש בשירות אינטרנט מרוחק, ויכול להיות שתצטרכו להגדיר הרשאות נוספות לחשבון השירות הזה דרך מסוף Google Cloud.

שימוש בקובץ JSON של חשבון שירות

קובצי JSON של חשבונות שירות מכילים את כל המידע שקשור לחשבונות שירות (כולל המפתח הפרטי של RSA). אפשר להוריד אותם ממסוף Firebase. כדי לקבל מידע נוסף על האופן שבו מאתחלים את SDK לאדמינים באמצעות קובץ JSON של חשבון שירות, פועלים לפי ההוראות להגדרת SDK לאדמינים.

שיטת האיניציאליזציה הזו מתאימה למגוון רחב של פריסות של SDK לאדמינים. הוא גם מאפשר ל-SDK לאדמינים ליצור ולחתום על אסימונים בהתאמה אישית באופן מקומי, בלי לבצע קריאות ל-API מרחוק. החיסרון העיקרי בגישה הזו הוא שצריך לארוז קובץ JSON של חשבון שירות יחד עם הקוד. חשוב גם לזכור שהמפתח הפרטי בקובץ ה-JSON של חשבון השירות הוא מידע רגיש, וצריך להקפיד לשמור על הסודיות שלו. באופן ספציפי, אל תוסיפו קובצי JSON של חשבונות שירות לניהול גרסאות ציבורי.

איך מאפשרים ל-SDK לאדמינים לגלות חשבון שירות

אם הקוד שלכם פרוס בסביבה שמנוהלת על ידי Google, ה-SDK לאדמינים יכול לנסות לגלות באופן אוטומטי אמצעי לחתימה על אסימונים בהתאמה אישית:

  • אם הקוד שלכם פרוס בסביבה הרגילה של App Engine ל-Java, ל-Python או ל-Go, ה-SDK לאדמינים יכול להשתמש בשירות App Identity שקיים בסביבה הזו כדי לחתום על טוקנים בהתאמה אישית. שירות App Identity חותם על נתונים באמצעות חשבון שירות שהוקצה לאפליקציה על ידי Google App Engine.

  • אם הקוד שלכם פרוס בסביבה מנוהלת אחרת (למשל, Google Cloud Functions או Google Compute Engine), ה-SDK של Firebase לאדמינים יכול לגלות באופן אוטומטי מחרוזת של מזהה חשבון שירות משרת המטא-נתונים המקומי. מזהה חשבון השירות שנמצא משמש בשילוב עם שירות IAM כדי לחתום על אסימונים מרחוק.

כדי להשתמש בשיטות החתימה האלה, צריך להפעיל את ה-SDK עם פרטי הכניסה שמוגדרים כברירת מחדל לאפליקציה (ADC) של Google, ולא לציין מחרוזת של מזהה חשבון שירות:

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 כך שיצביע עליו.

אם ה-SDK של Firebase לאדמינים צריך לגלות מחרוזת של מזהה חשבון שירות, הוא עושה את זה כשהקוד יוצר טוקן בהתאמה אישית בפעם הראשונה. התוצאה נשמרת במטמון ומשמשת לפעולות הבאות של חתימת טוקנים. מזהה חשבון השירות שמתגלה באופן אוטומטי הוא בדרך כלל אחד מחשבונות השירות שמוגדרים כברירת מחדל ומסופקים על ידי Google Cloud:

כמו במקרה של מזהי חשבונות שירות שצוינו במפורש, גם למזהי חשבונות שירות שזוהו באופן אוטומטי צריכה להיות ההרשאה iam.serviceAccounts.signBlob כדי שיצירת הטוקן בהתאמה אישית תפעל. יכול להיות שתצטרכו להשתמש בקטע IAM and admin במסוף Google Cloud כדי לתת לחשבונות השירות שמוגדרים כברירת מחדל את ההרשאות הנדרשות. מידע נוסף זמין בסעיף הבא, שעוסק בפתרון בעיות.

שימוש במזהה חשבון שירות

כדי לשמור על עקביות בין החלקים השונים של האפליקציה, אתם יכולים לציין מזהה של חשבון שירות שהמפתחות שלו ישמשו לחתימה על אסימונים בזמן הפעלה בסביבה שמנוהלת על ידי Google. כך אפשר לפשט את מדיניות ה-IAM ולהפוך אותה למאובטחת יותר, וגם להימנע מהצורך לכלול בקוד את קובץ ה-JSON של חשבון השירות.

מזהה חשבון השירות מופיע במסוף Google Cloud או בשדה client_email בקובץ ה-JSON של חשבון השירות שהורדתם. המזהים של חשבונות השירות הם כתובות אימייל בפורמט הבא: <client-id>@<project-id>.iam.gserviceaccount.com. הם מזהים באופן ייחודי חשבונות שירות ב-Firebase ובפרויקטים ב-Google Cloud.

כדי ליצור אסימונים בהתאמה אישית באמצעות מזהה נפרד של חשבון שירות, צריך להפעיל את ה-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",
});

מזהי חשבונות שירות הם לא מידע רגיש, ולכן אין משמעות לחשיפה שלהם. עם זאת, כדי לחתום על טוקנים בהתאמה אישית באמצעות חשבון השירות שצוין, ה-SDK של Firebase לאדמינים צריך להפעיל שירות מרוחק. בנוסף, צריך לוודא שלחשבון השירות שבו משתמש ה-SDK לאדמינים כדי לבצע את הקריאה הזו (בדרך כלל {project-name}@appspot.gserviceaccount.com) יש את ההרשאה iam.serviceAccounts.signBlob. מידע נוסף זמין בסעיף הבא, שעוסק בפתרון בעיות.

יצירת טוקנים בהתאמה אישית באמצעות SDK של Firebase לאדמינים

ל-SDK של Firebase לאדמינים יש שיטה מובנית ליצירת טוקנים בהתאמה אישית. לכל הפחות, צריך לספק 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()), האובייקט auth ב-Realtime Database Security Rules והאובייקט request.auth ב-Cloud Storage Security Rules יאוכלסו ב-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 של צד שלישי

אם ה-backend שלכם כתוב בשפה שאין לה SDK רשמי של Firebase לאדמינים, אתם עדיין יכולים ליצור אסימונים בהתאמה אישית באופן ידני. קודם כול, מוצאים ספריית JWT של צד שלישי בשפה שלכם. לאחר מכן, משתמשים בספריית ה-JWT הזו כדי ליצור JWT שכולל את ההצהרות הבאות:

הצהרות טוקן בהתאמה אישית
alg אלגוריתם "RS256"
iss המוסד המנפיק כתובת האימייל של חשבון השירות של הפרויקט
sub נושא כתובת האימייל של חשבון השירות של הפרויקט
aud קהל "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"
iat שעת ההנפקה השעה הנוכחית, בשניות מאז ראשית זמן יוניקס (Unix epoch)
exp מועד תפוגה הזמן, בשניות מאז ראשית זמן יוניקס (Unix epoch), שבו פג תוקף האסימון. הוא יכול להיות עד 3,600 שניות אחרי iat.
הערה: ההגדרה הזו קובעת רק את הזמן שבו יפוג התוקף של האסימון בהתאמה אישית עצמו. אבל אחרי שמשתמש נכנס באמצעות signInWithCustomToken(), הוא נשאר מחובר למכשיר עד שהסשן שלו מבוטל או עד שהוא יוצא מהחשבון.
uid המזהה הייחודי של המשתמש המחובר חייב להיות מחרוזת באורך של 1-128 תווים, כולל. ככל ש-uid קצר יותר, הביצועים טובים יותר.
ֶclaims (אופציונלי) הצהרות אופציונליות בהתאמה אישית שייכללו במשתנים auth / request.auth של כללי האבטחה

הנה כמה דוגמאות להטמעות של יצירת אסימונים בהתאמה אישית במגוון שפות שלא נתמכות ב-SDK של Firebase לאדמינים:

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 לא מופעל

אם אתם מציינים מזהה של חשבון שירות לחתימה על אסימונים, יכול להיות שתקבלו שגיאה דומה לזו:

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.

ה-SDK של Firebase לאדמינים משתמש ב-IAM API כדי לחתום על טוקנים. השגיאה הזו מציינת ש-IAM API לא מופעל כרגע בפרויקט ב-Firebase. פותחים את הקישור שבהודעת השגיאה בדפדפן אינטרנט ולוחצים על הכפתור Enable API כדי להפעיל אותו בפרויקט.

לחשבון השירות אין את ההרשאות הנדרשות

אם לחשבון השירות שבו פועל ה-SDK של Firebase לאדמינים אין את ההרשאה 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 (דור ראשון): משתמש בחשבון השירות שמוגדר כברירת מחדל ב-App Engine.
  • ‫Cloud Functions (דור שני): משתמש בחשבון השירות שמוגדר כברירת מחדל ב-Compute Engine.
  1. במסוף Google Cloud, נכנסים לדף IAM and Admin.

  2. לוחצים על סמל העריכה של חשבון השירות שרוצים לעדכן.

  3. לוחצים על הוספת תפקיד.

  4. מקלידים "Service Account Token Creator" במסנן החיפוש ובוחרים את האפשרות הזו מהתוצאות.

  5. לוחצים על Save כדי לאשר את הקצאת התפקיד.

לפרטים נוספים על התהליך הזה, אפשר לעיין במסמכי התיעוד של IAM או ללמוד איך לעדכן תפקידים באמצעות כלי שורת הפקודה של Google Cloud.

לא הצלחנו לזהות את חשבון השירות

אם מופיעה הודעת שגיאה דומה לזו, סימן שה-SDK של Firebase לאדמינים לא אותחל כמו שצריך.

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 כדי לגלות באופן אוטומטי מזהה של חשבון שירות, ודאו שהקוד פרוס בסביבה מנוהלת של Google עם שרת מטא-נתונים. אחרת, צריך לציין את קובץ ה-JSON של חשבון השירות או את מזהה חשבון השירות במהלך האתחול של ה-SDK.