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

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

לשם כך, צריך ליצור נקודת קצה (endpoint) של שרת שמקבלת כניסה פרטי כניסה — כמו שם משתמש וסיסמה — ואם פרטי הכניסה חוקי, מחזירה 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 פונקציות ו-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, ה-SDK של האדמין יכול לנסות לאתר באופן אוטומטי אמצעי לחתימה על אסימונים מותאמים אישית:

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

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

כדי להשתמש בשיטות החתימה האלה, צריך להפעיל את ה-SDK ב-Google פרטי הכניסה של Application Default Credentials ולא מציינים מחרוזת של מזהה חשבון שירות:

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

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

בדומה לשיטות אחרות של כניסה לחשבון (כמו 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 של צד שלישי

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

הצהרות על אסימונים בהתאמה אישית
alg אלגוריתם "RS256"
iss המנפיק כתובת האימייל של חשבון השירות של הפרויקט
sub נושא כתובת האימייל של חשבון השירות של הפרויקט
aud קהל "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"
iat תאריך ההנפקה הזמן הנוכחי, בשניות מאז תחילת התקופה של UNIX
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.

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

אם מופיעה הודעת שגיאה שדומה לזו, ה-SDK של Firebase Admin לא אותחל כראוי.

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.