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

    לדוגמה, כדי להתחיל, אפשר להוסיף לפונקציה שלכם פונקציית no-op כמו זו שבהמשך:

    Node.js

    import {
      beforeUserCreated,
    } from "firebase-functions/v2/identity";
    
    export const beforecreated = beforeUserCreated((event) => {
      // TODO
      return;
    });
    

    Python

    @identity_fn.before_user_created()
    def created_noop(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
        return
    

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

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

    firebase deploy --only functions
    

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

קבלת מידע על משתמשים והקשר

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

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

שם תיאור דוגמה
locale האזור של האפליקציה. אפשר להגדיר את הלוקאל באמצעות ה-SDK של הלקוח או על ידי העברת כותרת הלוקאל ב-REST API. 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 בפונקציה. לדוגמה:

Node.js

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

throw new HttpsError('invalid-argument');

Python

raise https_fn.HttpsError(
    code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT)

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

Node.js

throw new HttpsError('permission-denied', 'Unauthorized request origin!');

Python

raise https_fn.HttpsError(
    code=https_fn.FunctionsErrorCode.PERMISSION_DENIED,
    message="Unauthorized request origin!"
)

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

Node.js

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");
  }
});

Python

# 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 עוטף את השגיאה ומחזיר אותה ללקוח כשגיאה פנימית. לדוגמה:

Node.js

throw new HttpsError('invalid-argument', "Unauthorized email");

Python

# 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")

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

JavaScript

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 ומוחזר ללקוח.

כדי לשנות משתמש, מחזירים אובייקט מ-event handler שמכיל את השדות שרוצים לשנות. אפשר לשנות את השדות הבאים:

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

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

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

Node.js

export const beforecreated = beforeUserCreated((event) => {
  return {
    // If no display name is provided, set it to "Guest".
    displayName: event.data.displayName || 'Guest'
  };
});

Python

@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")

אם רושמים גורם שמטפל באירועים גם ב-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

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

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

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

ספקי OIDC גנריים

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

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

דוגמה:

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

Google

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

  • אסימון מזהה
  • טוקן גישה
  • רענון הטוקן: מועבר רק אם מתבקשים הפרמטרים המותאמים אישית הבאים:
    • 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

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

GitHub

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

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

Microsoft

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

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

דוגמה:

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 להירשם לאפליקציה:

Node.js

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  if (!user?.email?.includes('@example.com')) {
    throw new HttpsError(
      'invalid-argument', 'Unauthorized email');
  }
});

Python

 @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",
           )

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

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

Node.js

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  if (user.email && !user.emailVerified) {
    throw new HttpsError(
      'invalid-argument', 'Unverified email');
  }
});

Python

@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.")

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

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

Node.js

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  if (user.email && !user.emailVerified && event.eventType.includes(':facebook.com')) {
    return {
      emailVerified: true,
    };
  }
});

Python

@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:

Node.js

export const beforesignedin = beforeUserSignedIn((event) => {
  if (isSuspiciousIpAddress(event.ipAddress)) {
    throw new HttpsError(
      'permission-denied', 'Unauthorized access!');
  }
});

Python

@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.")

הגדרת טענות מותאמות אישית וטענות של סשן

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

Node.js

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,
            },
        };
    }
});

Python

@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 שדרכה המשתמש נכנס לחשבון:

    Node.js

    export const beforesignedin = beforeUserSignedIn((event) => {
      return {
        sessionClaims: {
          signInIpAddress: event.ipAddress,
        },
      };
    });
    

    Python

    @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 ששימשה לכניסה:

    Node.js

    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!' })
          });
        }
      });
    });
    

    Python

    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)
    

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

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

Node.js

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,
          };
        }
      });
});

Python

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

Node.js

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();
          });
      });
    })
  }
});

Python

@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 Authentication.

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