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

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

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

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

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

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

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

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

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

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

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

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

אם ה-Admin 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",
});

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

אם שפת ה-Backend שלכם לא כוללת 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). התאריך יכול להיות עד 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 כדי לחתום על טוקנים. השגיאה הזו מציינת שממשק ה-API של IAM לא מופעל כרגע בפרויקט 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 ‏'Service Account Token Creator' (יצירת אסימונים בחשבון שירות), בדרך כלל {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.