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

Firebase מאפשרת לכם לאמת משתמשים או מכשירים באמצעות אסימוני JWT (JSON Web Tokens) מאובטחים, וכך נותנת לכם שליטה מלאה על תהליך האימות. אתם יוצרים את האסימונים האלה בשרת, מעבירים אותם בחזרה למכשיר של הלקוח ומשתמשים בהם כדי לבצע אימות באמצעות method 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 Admin, או להשתמש בספריית JWT של צד שלישי אם השרת נכתב בשפה שאין לה תמיכה מובנית ב-Firebase.

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

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

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

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

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

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

לאפשר ל-Admin SDK לגלות חשבון שירות

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

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

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

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

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

יוצרים אסימונים בהתאמה אישית באמצעות 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()), האובייקט 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 של צד שלישי

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

ריכזנו כאן כמה דוגמאות להטמעה של יצירת אסימונים מותאמים אישית במגוון שפות שלא נתמכות ב-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 לא מופעל

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

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 שלכם. פותחים את הקישור שמופיע בהודעת השגיאה בדפדפן אינטרנט ולוחצים על הלחצן 'הפעלת ה-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. פותחים את הדף IAM and admin במסוף Google Cloud.
  2. בוחרים את הפרויקט ולוחצים על 'המשך'.
  3. לוחצים על סמל העריכה של חשבון השירות שרוצים לעדכן.
  4. לוחצים על 'הוספת תפקיד נוסף'.
  5. מקלידים Service Account Token Creator במסנן החיפוש ובוחרים אותו מהתוצאות.
  6. לוחצים על 'שמירה' כדי לאשר את הקצאת התפקיד.

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