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

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 שבו ה-SDK של Firebase Admin ישתמש כדי לחתום על אסימונים מותאמים אישית:

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

ל-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 של צד שלישי

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