הרחבת האימות ב-Firebase באמצעות פונקציות חסימה


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

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

כדי להשתמש בפונקציות חסימה צריך לשדרג את פרויקט Firebase ל-Firebase Authentication with Identity Platform. אם עדיין לא שדרגתם, עליכם לעשות זאת קודם.

הסבר על פונקציות חסימה

אפשר לרשום פונקציות חסימה לאירועים האלה:

  • לפני שהמשתמש נוצר: מופעל לפני שמשתמש חדש נשמר במסד הנתונים Firebase Authentication ולפני שאסימון מוחזר לאפליקציית הלקוח.

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

  • לפני שליחת אימייל (Node.js בלבד): הטריגר מופעל לפני שליחת אימייל (למשל,
    אימייל לכניסה או לאיפוס סיסמה) למשתמש.

  • לפני שליחת הודעת SMS (Node.js בלבד): הטריגר מופעל לפני שליחת הודעת SMS למשתמש, במקרים כמו אימות רב-גורמי.

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

  • הפונקציה צריכה להשיב תוך 7 שניות. אחרי 7 שניות, הפונקציה Firebase Authentication מחזירה שגיאה והפעולה של הלקוח נכשלת.

  • קודי תגובה של HTTP שאינם 200 מועברים לאפליקציות הלקוח. חשוב לוודא שקוד הלקוח מטפל בכל השגיאות שהפונקציה עשויה להחזיר.

  • הפונקציות חלות על כל המשתמשים בפרויקט, כולל משתמשים בדייר. Firebase Authentication מספק מידע על משתמשים לפונקציה שלכם, כולל כל הדיירים שהם שייכים אליהם, כדי שתוכלו להגיב בהתאם.

  • קישור של ספק זהויות אחר לחשבון מפעיל מחדש את כל הפונקציות הרשמות של beforeUserSignedIn.

  • אימות אנונימי ומותאם אישית לא מפעיל פונקציות חסימה.

פריסת פונקציית חסימה

כדי להוסיף את הקוד המותאם אישית לתהליכי אימות המשתמש, צריך לפרוס פונקציות חסימה. אחרי הפריסה של פונקציות החסימה, הקוד בהתאמה אישית צריך להשלים את הפעולה בהצלחה כדי שהאימות ויצירת המשתמש יתבצעו.

פורסים פונקציית חסימה באותו אופן שבו פורסים כל פונקציה. (לפרטים, אפשר לעיין בדף Cloud Functions תחילת העבודה). בקצרה:

  1. כותבים פונקציה שמטפלת באירוע המטורגט.

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

    import {
      beforeUserCreated,
    } from "firebase-functions/v2/identity";
    
    export const beforecreated = beforeUserCreated((event) => {
      // TODO
      return;
    });
    
    @identity_fn.before_user_created()
    def created_noop(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
        return
    

    בדוגמה שלמעלה לא הוספנו את ההטמעה של לוגיקה מותאמת אישית לאימות. בקטע הבא מוסבר איך מטמיעים את פונקציות החסימה, ובתרחישי נפוצים מפורטות דוגמאות ספציפיות.

  2. פורסים את הפונקציות באמצעות CLI של Firebase:

    firebase deploy --only functions
    

    צריך לפרוס מחדש את הפונקציות בכל פעם שמעדכנים אותן.

אחזור פרטי משתמשים והקשר

אירועי חסימה מספקים אובייקט AuthBlockingEvent שמכיל מידע על המשתמש שנכנס לחשבון. תוכלו להשתמש בערכים האלו בקוד כדי לקבוע אם לאפשר לפעולה להמשיך.

האובייקט מכיל את המאפיינים הבאים:

שם תיאור דוגמה
locale אזור הישות של האפליקציה. אפשר להגדיר את השפה באמצעות ה-SDK של הלקוח, או להעביר את הכותרת של השפה ב-API ל-REST. fr או sv-SE
ipAddress כתובת ה-IP של המכשיר שמשתמש הקצה נרשם או נכנס ממנו. 114.14.200.1
userAgent סוכן המשתמש שמפעיל את פונקציית החסימה. Mozilla/5.0 (X11; Linux x86_64)
eventId המזהה הייחודי של האירוע. rWsyPtolplG2TBFoOkkgyg
eventType סוג האירוע. יוצג מידע על שם האירוע, כמו beforeSignIn או beforeCreate, ועל שיטת הכניסה שמשויכת אליה, כמו Google או כתובת אימייל/סיסמה. providers/cloud.auth/eventTypes/user.beforeSignIn:password
authType תמיד USER. USER
resource הפרויקט או הדייר ב-Firebase Authentication. projects/project-id/tenants/tenant-id
timestamp שעת ההפעלה של האירוע, בפורמט RFC 3339. Tue, 23 Jul 2019 21:10:57 GMT
additionalUserInfo אובייקט שמכיל מידע על המשתמש. AdditionalUserInfo
credential אובייקט שמכיל מידע על פרטי הכניסה של המשתמש. AuthCredential

חסימת רישום או כניסה

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

import { HttpsError } from "firebase-functions/v2/identity";

throw new HttpsError('invalid-argument');
raise https_fn.HttpsError(
    code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT)

אפשר גם לציין הודעת שגיאה בהתאמה אישית:

throw new HttpsError('permission-denied', 'Unauthorized request origin!');
raise https_fn.HttpsError(
    code=https_fn.FunctionsErrorCode.PERMISSION_DENIED,
    message="Unauthorized request origin!"
)

הדוגמה הבאה מראה איך למנוע ממשתמשים שלא נמצאים בדומיין מסוים להירשם לאפליקציה:

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  // (If the user is authenticating within a tenant context, the tenant ID can be determined from
  // user.tenantId or from event.resource, e.g. 'projects/project-id/tenant/tenant-id-1')

  // Only users of a specific domain can sign up.
  if (!user?.email?.includes('@acme.com')) {
    throw new HttpsError('invalid-argument', "Unauthorized email");
  }
});
# Block account creation with any non-acme email address.
@identity_fn.before_user_created()
def validatenewuser(
        event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
    # User data passed in from the CloudEvent.
    user = event.data

    # Only users of a specific domain can sign up.
    if user.email is None or "@acme.com" not in user.email:
        # Return None so that Firebase Auth rejects the account creation.
        raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
                                  message="Unauthorized email")

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

throw new HttpsError('invalid-argument', "Unauthorized email");
# Only users of a specific domain can sign up.
if user.email is None or "@acme.com" not in user.email:
    # Return None so that Firebase Auth rejects the account creation.
    raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
                              message="Unauthorized email")

האפליקציה אמורה לזהות את השגיאה ולטפל בה בהתאם. לדוגמה:

import { getAuth, createUserWithEmailAndPassword } from 'firebase/auth';

// Blocking functions can also be triggered in a multi-tenant context before user creation.
// firebase.auth().tenantId = 'tenant-id-1';
const auth = getAuth();
try {
  const result = await createUserWithEmailAndPassword(auth)
  const idTokenResult = await result.user.getIdTokenResult();
  console.log(idTokenResult.claim.admin);
} catch(error) {
  if (error.code !== 'auth/internal-error' && error.message.indexOf('Cloud Function') !== -1) {
      // Display error.
    } else {
      // Registration succeeds.
    }
}

שינוי משתמש

במקום לחסום רישום או ניסיון כניסה, אפשר לאפשר להמשך את הפעולה, אבל לשנות את האובייקט User שנשמר במסד הנתונים של Firebase Authentication ומוחזר ללקוח.

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

  • displayName
  • disabled
  • emailVerified
  • photoUrl
  • customClaims
  • sessionClaims (beforeUserSignedIn בלבד)

פרט ל-sessionClaims, כל השדות ששונו נשמרים במסד הנתונים של Firebase Authentication, כלומר הם כלולים באסימון התגובה ונשמרים בין סשנים של משתמשים.

הדוגמה הבאה מראה איך מגדירים שם תצוגה שמוגדר כברירת מחדל:

export const beforecreated = beforeUserCreated((event) => {
  return {
    // If no display name is provided, set it to "Guest".
    displayName: event.data.displayName || 'Guest'
  };
});
@identity_fn.before_user_created()
def setdefaultname(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
    return identity_fn.BeforeCreateResponse(
        # If no display name is provided, set it to "Guest".
        display_name=event.data.display_name if event.data.display_name is not None else "Guest")

אם רושמים handler של אירועים גם ל-beforeUserCreated וגם ל-beforeUserSignedIn, שימו לב שה-beforeUserSignedIn יופעל אחרי beforeUserCreated. שדות של משתמשים שעודכנו ב-beforeUserCreated גלויים ב-beforeUserSignedIn. אם מגדירים שדה שהוא לא sessionClaims בשני הגורמים המטפלים באירועים, הערך שהוגדר ב-beforeUserSignedIn יחליף את הערך שהוגדר ב-beforeUserCreated. עבור sessionClaims בלבד, הם מועברים לטענות הנכונות של האסימון של הסשן הנוכחי, אבל לא נשמרים או מאוחסנים במסד הנתונים.

לדוגמה, אם מוגדרים ערכים של sessionClaims, הפונקציה beforeUserSignedIn תחזיר אותם יחד עם הצהרות beforeUserCreated, והן ימוזגו. כשהם ממוזגים, אם מפתח sessionClaims תואם למפתח ב-customClaims, המפתח התואם ב-customClaims יימחק בהצהרות האסימון על ידי המפתח sessionClaims. עם זאת, המפתח customClaims שעבר כתיבה מחדש עדיין יישמר במסד הנתונים לצורך בקשות עתידיות.

פרטי כניסה ונתונים נתמכים של OAuth

אפשר להעביר פרטי כניסה ונתונים של OAuth לפונקציות חסימה מספק זהויות שונים. בטבלה הבאה אפשר לראות אילו פרטי כניסה ונתונים נתמכים לכל ספק זהויות:

ספק זהות (IdP) אסימון מזהה טוקן גישה מועד תפוגה סוד הטוקן טוקן רענון הצהרות על כניסה
Google כן כן כן לא כן לא
Facebook לא כן כן לא לא לא
Twitter לא כן לא כן לא לא
GitHub לא כן לא לא לא לא
Microsoft כן כן כן לא כן לא
LinkedIn לא כן כן לא לא לא
Yahoo כן כן כן לא כן לא
Apple כן כן כן לא כן לא
SAML לא לא לא לא לא כן
OIDC כן כן כן לא כן כן

אסימוני OAuth

כדי להשתמש באסימון מזהה, באסימון גישה או באסימון רענון בפונקציית חסימה, צריך קודם לסמן את התיבה בדף Blocking functions במסוף Firebase.

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

בקטעים הבאים מתוארים כל סוגי ספקי הזהויות ופרטי הכניסה והנתונים הנתמכים שלהם.

ספקי OIDC גנריים

כשמשתמש נכנס באמצעות ספק OIDC גנרי, פרטי הכניסה הבאים מועברים:

  • אסימון מזהה: מסופק אם נבחר התהליך id_token.
  • אסימון גישה: מסופק אם בוחרים בתהליך הקוד. שימו לב שתהליך הקוד נתמך כרגע רק דרך API ל-REST.
  • Refresh token: אסימון שמסופק אם נבחר ההיקף offline_access.

דוגמה:

const provider = new firebase.auth.OAuthProvider('oidc.my-provider');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);

Google

כשמשתמש נכנס באמצעות חשבון Google, פרטי הכניסה הבאים מועברים:

  • אסימון מזהה
  • אסימון גישה
  • Refresh token: הנתון הזה מסופק רק אם מבקשים את הפרמטרים המותאמים אישית הבאים:
    • access_type=offline
    • prompt=consent, אם המשתמש הביע הסכמה בעבר ולא נשלחה בקשה להיקף חדש

דוגמה:

import { getAuth, signInWithPopup, GoogleAuthProvider } from 'firebase/auth';

const auth = getAuth();
const provider = new GoogleAuthProvider();
provider.setCustomParameters({
  'access_type': 'offline',
  'prompt': 'consent'
});
signInWithPopup(auth, provider);

מידע נוסף על אסימוני הרענון של Google

Facebook

כשמשתמש נכנס באמצעות Facebook, פרטי הכניסה הבאים מועברים:

  • אסימון גישה: מוחזר אסימון גישה שאפשר להחליף באסימון גישה אחר. מידע נוסף על הסוגים השונים של אסימוני גישה שנתמכים על ידי Facebook, ועל האופן שבו אפשר להמיר אותם לאסימונים לטווח ארוך.

GitHub

כשמשתמש נכנס באמצעות GitHub, פרטי הכניסה הבאים יועברו:

  • אסימון גישה: תוקפו לא יפוג אלא אם יבוטל.

Microsoft

כשמשתמש נכנס באמצעות Microsoft, פרטי הכניסה הבאים יועברו:

  • אסימון מזהה
  • אסימון גישה
  • אסימון רענון: מועבר לפונקציית החסימה אם נבחר היקף offline_access.

דוגמה:

import { getAuth, signInWithPopup, OAuthProvider } from 'firebase/auth';

const auth = getAuth();
const provider = new OAuthProvider('microsoft.com');
provider.addScope('offline_access');
signInWithPopup(auth, provider);

Yahoo

כשמשתמש נכנס באמצעות Yahoo, פרטי הכניסה הבאים יועברו ללא פרמטרים או היקפים מותאמים אישית:

  • אסימון מזהה
  • אסימון גישה
  • אסימון רענון

LinkedIn

כשמשתמש נכנס באמצעות LinkedIn, פרטי הכניסה הבאים יועברו:

  • אסימון גישה

Apple

כשמשתמש נכנס באמצעות Apple, פרטי הכניסה הבאים יועברו ללא פרמטרים או היקפים מותאמים אישית:

  • אסימון מזהה
  • אסימון גישה
  • אסימון רענון

תרחישים נפוצים

הדוגמאות הבאות מציגות תרחישים נפוצים לדוגמה לפונקציות חסימה:

איך מאפשרים רישום רק מדומיין ספציפי

בדוגמה הבאה אפשר לראות איך למנוע ממשתמשים שאינם בדומיין example.com להירשם לאפליקציה שלך:

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  if (!user?.email?.includes('@example.com')) {
    throw new HttpsError(
      'invalid-argument', 'Unauthorized email');
  }
});
 @identity_fn.before_user_created()
   def validatenewuser(
       event: identity_fn.AuthBlockingEvent,
   ) -> identity_fn.BeforeCreateResponse | None:
       # User data passed in from the CloudEvent.
       user = event.data

       # Only users of a specific domain can sign up.
       if user.email is None or "@example.com" not in user.email:
           # Return None so that Firebase Auth rejects the account creation.
           raise https_fn.HttpsError(
               code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
               message="Unauthorized email",
           )

חסימה של משתמשים עם כתובות אימייל לא מאומתות

בדוגמה הבאה מוסבר איך למנוע ממשתמשים עם כתובות אימייל לא מאומתות להירשם לאפליקציה:

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  if (user.email && !user.emailVerified) {
    throw new HttpsError(
      'invalid-argument', 'Unverified email');
  }
});
@identity_fn.before_user_created()
def requireverified(
        event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
    if event.data.email is not None and not event.data.email_verified:
        raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
                                  message="You must register using a trusted provider.")

התייחסות לאימיילים של ספקי זהויות מסוימים כמאומתים

בדוגמה הבאה מוסבר איך להתייחס לכתובות אימייל של משתמשים מספקי זהויות מסוימים כאל כתובות מאומתות:

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  if (user.email && !user.emailVerified && event.eventType.includes(':facebook.com')) {
    return {
      emailVerified: true,
    };
  }
});
@identity_fn.before_user_created()
def markverified(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
    if event.data.email is not None and "@facebook.com" in event.data.email:
        return identity_fn.BeforeSignInResponse(email_verified=True)

חסימת כניסה מכתובות IP מסוימות

הדוגמה הבאה מראה איך לחסום כניסה מטווחים מסוימים של כתובות IP:

export const beforesignedin = beforeUserSignedIn((event) => {
  if (isSuspiciousIpAddress(event.ipAddress)) {
    throw new HttpsError(
      'permission-denied', 'Unauthorized access!');
  }
});
@identity_fn.before_user_signed_in()
def ipban(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSignInResponse | None:
    if is_suspicious(event.ip_address):
        raise https_fn.HttpsError(code=https_fn.FunctionsErrorCode.PERMISSION_DENIED,
                                  message="IP banned.")

הגדרת הצהרות מותאמות אישית והצהרות סשן

הדוגמה הבאה מראה איך להגדיר הצהרות זכויות יוצרים בהתאמה אישית והצהרות זכויות יוצרים של סשן:

export const beforecreated = beforeUserCreated((event) => {
    if (event.credential &&
        event.credential.claims &&
        event.credential.providerId === "saml.my-provider-id") {
        return {
            // Employee ID does not change so save in persistent claims (stored in
            // Auth DB).
            customClaims: {
                eid: event.credential.claims.employeeid,
            },
        };
    }
});

export const beforesignin = beforeUserSignedIn((event) => {
    if (event.credential &&
        event.credential.claims &&
        event.credential.providerId === "saml.my-provider-id") {
        return {
            // Copy role and groups to token claims. These will not be persisted.
            sessionClaims: {
                role: event.credential.claims.role,
                groups: event.credential.claims.groups,
            },
        };
    }
});
@identity_fn.before_user_created()
def setemployeeid(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
    if (event.credential is not None and event.credential.claims is not None and
            event.credential.provider_id == "saml.my-provider-id"):
        return identity_fn.BeforeCreateResponse(
            custom_claims={"eid": event.credential.claims["employeeid"]})


@identity_fn.before_user_signed_in()
def copyclaimstosession(
        event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSignInResponse | None:
    if (event.credential is not None and event.credential.claims is not None and
            event.credential.provider_id == "saml.my-provider-id"):
        return identity_fn.BeforeSignInResponse(session_claims={
            "role": event.credential.claims["role"],
            "groups": event.credential.claims["groups"]
        })

מעקב אחר כתובות IP כדי לעקוב אחרי פעילות חשודה

כדי למנוע גניבת אסימונים, אפשר לעקוב אחרי כתובת ה-IP שממנה המשתמש נכנס לחשבון ולהשוות אותה לכתובת ה-IP בבקשות הבאות. אם הבקשה נראית חשודה – לדוגמה, כתובות ה-IP מגיעות מאזורים גיאוגרפיים שונים – אפשר לבקש מהמשתמש להיכנס שוב.

  1. משתמשים בהצהרות על סשנים כדי לעקוב אחרי כתובת ה-IP שבאמצעותה המשתמש נכנס לחשבון:

    export const beforesignedin = beforeUserSignedIn((event) => {
      return {
        sessionClaims: {
          signInIpAddress: event.ipAddress,
        },
      };
    });
    
    @identity_fn.before_user_signed_in()
    def logip(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSignInResponse | None:
        return identity_fn.BeforeSignInResponse(session_claims={"signInIpAddress": event.ip_address})
    
  2. כשמשתמש מנסה לגשת למשאבים שדורשים אימות באמצעות Firebase Authentication, צריך להשוות בין כתובת ה-IP בבקשה לכתובת ה-IP ששימשה לכניסה:

    app.post('/getRestrictedData', (req, res) => {
      // Get the ID token passed.
      const idToken = req.body.idToken;
      // Verify the ID token, check if revoked and decode its payload.
      admin.auth().verifyIdToken(idToken, true).then((claims) => {
        // Get request IP address
        const requestIpAddress = req.connection.remoteAddress;
        // Get sign-in IP address.
        const signInIpAddress = claims.signInIpAddress;
        // Check if the request IP address origin is suspicious relative to
        // the session IP addresses. The current request timestamp and the
        // auth_time of the ID token can provide additional signals of abuse,
        // especially if the IP address suddenly changed. If there was a sudden
        // geographical change in a short period of time, then it will give
        // stronger signals of possible abuse.
        if (!isSuspiciousIpAddressChange(signInIpAddress, requestIpAddress)) {
          // Suspicious IP address change. Require re-authentication.
          // You can also revoke all user sessions by calling:
          // admin.auth().revokeRefreshTokens(claims.sub).
          res.status(401).send({error: 'Unauthorized access. Please login again!'});
        } else {
          // Access is valid. Try to return data.
          getData(claims).then(data => {
            res.end(JSON.stringify(data);
          }, error => {
            res.status(500).send({ error: 'Server error!' })
          });
        }
      });
    });
    
    from firebase_admin import auth, initialize_app
    import flask
    
    initialize_app()
    flask_app = flask.Flask(__name__)
    
    @flask_app.post()
    def get_restricted_data(req: flask.Request):
        # Get the ID token passed.
        id_token = req.json().get("idToken")
    
        # Verify the ID token, check if revoked, and decode its payload.
        try:
            claims = auth.verify_id_token(id_token, check_revoked=True)
        except:
            return flask.Response(status=500)
    
        # Get request IP address.
        request_ip = req.remote_addr
    
        # Get sign-in IP address.
        signin_ip = claims["signInIpAddress"]
    
        # Check if the request IP address origin is suspicious relative to
        # the session IP addresses. The current request timestamp and the
        # auth_time of the ID token can provide additional signals of abuse,
        # especially if the IP address suddenly changed. If there was a sudden
        # geographical change in a short period of time, then it will give
        # stronger signals of possible abuse.
        if is_suspicious_change(signin_ip, request_ip):
            # Suspicious IP address change. Require re-authentication.
            # You can also revoke all user sessions by calling:
            #   auth.revoke_refresh_tokens(claims["sub"])
            return flask.Response(status=401,
                                  response="Unauthorized access. Sign in again!")
        else:
            # Access is valid. Try to return data.
            return data_from_claims(claims)
    

בדיקת תמונות של משתמשים

הדוגמה הבאה מראה איך לחיטוי תמונות פרופיל של משתמשים:

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  if (user.photoURL) {
    return isPhotoAppropriate(user.photoURL)
      .then((status) => {
        if (!status) {
          // Sanitize inappropriate photos by replacing them with guest photos.
          // Users could also be blocked from sign-up, disabled, etc.
          return {
            photoUrl: PLACEHOLDER_GUEST_PHOTO_URL,
          };
        }
      });
});
@identity_fn.before_user_created()
def sanitizeprofilephoto(
        event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
    if event.data.photo_url is not None:
        score = analyze_photo_with_ml(event.data.photo_url)
        if score > THRESHOLD:
            return identity_fn.BeforeCreateResponse(photo_url=PLACEHOLDER_URL)

למידע נוסף על זיהוי תמונות וניפוי שלהן, קראו את המאמרים בנושא Cloud Vision.

גישה לפרטי כניסה בפרוטוקול OAuth של ספק הזהויות של המשתמש

בדוגמה הבאה תוכלו לראות איך מקבלים אסימון רענון למשתמש שנכנס באמצעות חשבון Google ומשתמשים בו כדי לשלוח קריאה לממשקי ה-API של יומן Google. אסימון הרענון מאוחסן לגישה אופליין.

const {OAuth2Client} = require('google-auth-library');
const {google} = require('googleapis');
// ...
// Initialize Google OAuth client.
const keys = require('./oauth2.keys.json');
const oAuth2Client = new OAuth2Client(
  keys.web.client_id,
  keys.web.client_secret
);

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  if (event.credential &&
      event.credential.providerId === 'google.com') {
    // Store the refresh token for later offline use.
    // These will only be returned if refresh tokens credentials are included
    // (enabled by Cloud console).
    return saveUserRefreshToken(
        user.uid,
        event.credential.refreshToken,
        'google.com'
      )
      .then(() => {
        // Blocking the function is not required. The function can resolve while
        // this operation continues to run in the background.
        return new Promise((resolve, reject) => {
          // For this operation to succeed, the appropriate OAuth scope should be requested
          // on sign in with Google, client-side. In this case:
          // https://www.googleapis.com/auth/calendar
          // You can check granted_scopes from within:
          // event.additionalUserInfo.profile.granted_scopes (space joined list of scopes).

          // Set access token/refresh token.
          oAuth2Client.setCredentials({
            access_token: event.credential.accessToken,
            refresh_token: event.credential.refreshToken,
          });
          const calendar = google.calendar('v3');
          // Setup Onboarding event on user's calendar.
          const event = {/** ... */};
          calendar.events.insert({
            auth: oauth2client,
            calendarId: 'primary',
            resource: event,
          }, (err, event) => {
            // Do not fail. This is a best effort approach.
            resolve();
          });
      });
    })
  }
});
@identity_fn.before_user_created()
def savegoogletoken(
        event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
    """During sign-up, save the Google OAuth2 access token and queue up a task
    to schedule an onboarding session on the user's Google Calendar.

    You will only get an access token if you enabled it in your project's blocking
    functions settings in the Firebase console:

    https://console.firebase.google.com/project/_/authentication/settings
    """
    if event.credential is not None and event.credential.provider_id == "google.com":
        print(f"Signed in with {event.credential.provider_id}. Saving access token.")

        firestore_client: google.cloud.firestore.Client = firestore.client()
        doc_ref = firestore_client.collection("user_info").document(event.data.uid)
        doc_ref.set({"calendar_access_token": event.credential.access_token}, merge=True)

        tasks_client = google.cloud.tasks_v2.CloudTasksClient()
        task_queue = tasks_client.queue_path(params.PROJECT_ID.value,
                                             options.SupportedRegion.US_CENTRAL1,
                                             "scheduleonboarding")
        target_uri = get_function_url("scheduleonboarding")
        calendar_task = google.cloud.tasks_v2.Task(http_request={
            "http_method": google.cloud.tasks_v2.HttpMethod.POST,
            "url": target_uri,
            "headers": {
                "Content-type": "application/json"
            },
            "body": json.dumps({
                "data": {
                    "uid": event.data.uid
                }
            }).encode()
        },
                                                   schedule_time=datetime.now() +
                                                   timedelta(minutes=1))
        tasks_client.create_task(parent=task_queue, task=calendar_task)

ביטול ההחלטה של reCAPTCHA Enterprise לגבי פעולת משתמש

בדוגמה הבאה אפשר לראות איך לבטל קביעה של reCAPTCHA Enterprise עבור תהליכי עבודה נתמכים של משתמשים.

במאמר הפעלת reCAPTCHA Enterprise מוסבר בהרחבה איך לשלב את reCAPTCHA Enterprise עם אימות Firebase.

אפשר להשתמש בפונקציות החסימה כדי לאשר או לחסום תהליכים על סמך גורמים מותאמים אישית, ובכך לבטל את התוצאה שסופקה על ידי reCAPTCHA Enterprise.

Node.js
const { beforeSmsSent } = require("firebase-functions/v2/identity");
exports.beforesmssentv2 = beforeSmsSent((event) => {
 if (
   event.smsType === "SIGN_IN_OR_SIGN_UP" &&
   event.additionalUserInfo.phoneNumber.includes('+91')
 ) {
   return {
     recaptchaActionOverride: "ALLOW",
   };
 }

 // Allow users to sign in with recaptcha score greater than 0.5
 if (event.additionalUserInfo.recaptchaScore > 0.5) {
   return {
     recaptchaActionOverride: 'ALLOW',
   };
 }

 // Block all others.
 return  {
   recaptchaActionOverride: 'BLOCK',
 }
});