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


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

לפני שאתה מתחיל

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    לדוגמה, כדי להתחיל, אתה יכול להוסיף את הפונקציות הבאות ללא הפעלה ל- index.js :

    const functions = require('firebase-functions');
    
    exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
      // TODO
    });
    
    exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
      // TODO
    });
    

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

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

    firebase deploy --only functions
    

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

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

האירועים beforeSignIn ו- beforeCreate מספקים אובייקטי User ו- EventContext המכילים מידע על המשתמש שנכנס. השתמש בערכים אלה בקוד שלך כדי לקבוע אם לאפשר לפעולה להמשיך.

לרשימה של מאפיינים הזמינים באובייקט User , עיין בהפניה ל- UserRecord API .

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

שֵׁם תיאור דוגמא
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 או דייר. projects/ project-id /tenants/ tenant-id
timestamp הזמן שבו האירוע הופעל, מעוצב כמחרוזת RFC 3339 . Tue, 23 Jul 2019 21:10:57 GMT
additionalUserInfo אובייקט המכיל מידע על המשתמש. AdditionalUserInfo
credential אובייקט המכיל מידע על האישורים של המשתמש. AuthCredential

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

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

Node.js

throw new functions.auth.HttpsError('permission-denied');

הטבלה הבאה מפרטת את השגיאות שאתה יכול להעלות, יחד עם הודעת השגיאה המוגדרת כברירת מחדל:

שֵׁם קוד הוֹדָעָה
invalid-argument 400 הלקוח ציין ארגומנט לא חוקי.
failed-precondition 400 לא ניתן לבצע את הבקשה במצב המערכת הנוכחי.
out-of-range 400 הלקוח ציין טווח לא חוקי.
unauthenticated 401 אסימון OAuth חסר, לא חוקי או שפג תוקפו.
permission-denied 403 ללקוח אין הרשאה מספקת.
not-found 404 המשאב שצוין לא נמצא.
aborted 409 התנגשות במקביל, כגון התנגשות קריאה-שינוי-כתיבה.
already-exists 409 המשאב שלקוח ניסה ליצור כבר קיים.
resource-exhausted 429 או מחוץ למכסת המשאבים או הגעה להגבלת שיעור.
cancelled 499 הבקשה בוטלה על ידי הלקוח.
data-loss 500 אובדן נתונים בלתי ניתן לשחזור או השחתת נתונים.
unknown 500 שגיאת שרת לא ידועה.
internal 500 שגיאת שרת פנימית.
not-implemented 501 שיטת API לא מיושמת על ידי השרת.
unavailable 503 שירותים לא זמינים.
deadline-exceeded 504 חרג המועד האחרון לבקשה.

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

Node.js

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

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

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  // (If the user is authenticating within a tenant context, the tenant ID can be determined from
  // user.tenantId or from context.resource, e.g. 'projects/project-id/tenant/tenant-id-1')

  // Only users of a specific domain can sign up.
  if (user.email.indexOf('@acme.com') === -1) {
    throw new functions.auth.HttpsError('invalid-argument', `Unauthorized email "${user.email}"`);
  }
});

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

throw new functions.auth.HttpsError('invalid-argument', `Unauthorized email user@evil.com}`);

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

JavaScript

// Blocking functions can also be triggered in a multi-tenant context before user creation.
// firebase.auth().tenantId = 'tenant-id-1';
firebase.auth().createUserWithEmailAndPassword('johndoe@example.com', 'password')
  .then((result) => {
    result.user.getIdTokenResult()
  })
  .then((idTokenResult) => {
    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 ( beforeSignIn בלבד)

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

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

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  return {
    // If no display name is provided, set it to "Guest".
    displayName: user.displayName || 'Guest';
  };
});

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

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

אישורים ונתונים של OAuth נתמכים

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

ספק זהות אסימון מזהה אסימון גישה תאריך תפוגה סוד אסימון רענן אסימון תביעות כניסה
גוגל כן כן כן לא כן לא
פייסבוק לא כן כן לא לא לא
טוויטר לא כן לא כן לא לא
GitHub לא כן לא לא לא לא
מיקרוסופט כן כן כן לא כן לא
לינקדאין לא כן כן לא לא לא
יאהו כן כן כן לא כן לא
תפוח עץ כן כן כן לא כן לא
SAML לא לא לא לא לא כן
OIDC כן כן כן לא כן כן

רענן אסימונים

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

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

דוגמא:

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

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

פייסבוק

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

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

GitHub

כאשר משתמש נכנס ל-GitHub, האישורים הבאים יעברו:

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

מיקרוסופט

כאשר משתמש נכנס ל-Microsoft, האישורים הבאים יועברו:

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

דוגמא:

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

יאהו

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

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

לינקדאין

כאשר משתמש נכנס ל-LinkedIn, האישורים הבאים יעברו:

  • אסימון גישה

תפוח עץ

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

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

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

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

מאפשר רישום מדומיין ספציפי בלבד

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

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (!user.email || user.email.indexOf('@example.com') === -1) {
    throw new functions.auth.HttpsError(
      'invalid-argument', `Unauthorized email "${user.email}"`);
  }
});

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

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

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (user.email && !user.emailVerified) {
    throw new functions.auth.HttpsError(
      'invalid-argument', `Unverified email "${user.email}"`);
  }
});

דורש אימות דוא"ל בהרשמה

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

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  const locale = context.locale;
  if (user.email && !user.emailVerified) {
    // Send custom email verification on sign-up.
    return admin.auth().generateEmailVerificationLink(user.email).then((link) => {
      return sendCustomVerificationEmail(user.email, link, locale);
    });
  }
});

exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
 if (user.email && !user.emailVerified) {
   throw new functions.auth.HttpsError(
     'invalid-argument', `"${user.email}" needs to be verified before access is granted.`);
  }
});

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

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

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (user.email && !user.emailVerified && context.eventType.indexOf(':facebook.com') !== -1) {
    return {
      emailVerified: true,
    };
  }
});

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

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

Node.js

exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
  if (isSuspiciousIpAddress(context.ipAddress)) {
    throw new functions.auth.HttpsError(
      'permission-denied', 'Unauthorized access!');
  }
});

הגדרת תביעות מותאמות אישית והפעלה

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

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (context.credential &&
      context.credential.providerId === 'saml.my-provider-id') {
    return {
      // Employee ID does not change so save in persistent claims (stored in
      // Auth DB).
      customClaims: {
        eid: context.credential.claims.employeeid,
      },
      // Copy role and groups to token claims. These will not be persisted.
      sessionClaims: {
        role: context.credential.claims.role,
        groups: context.credential.claims.groups,
      }
    }
  }
});

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

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

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

    Node.js

    exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
      return {
        sessionClaims: {
          signInIpAddress: context.ipAddress,
        },
      };
    });
    
  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!' })
          });
        }
      });
    });
    

הקרנת תמונות משתמשים

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

Node.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  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,
          };
        }
      });
});

למידע נוסף על איך לזהות ולחטא תמונות, עיין בתיעוד של Cloud Vision .

גישה לאישורי OAuth של ספק הזהות של משתמש

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

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

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (context.credential &&
      context.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,
        context.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:
          // context.additionalUserInfo.profile.granted_scopes (space joined list of scopes).

          // Set access token/refresh token.
          oAuth2Client.setCredentials({
            access_token: context.credential.accessToken,
            refresh_token: context.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();
          });
      });
    })
  }
});

עוקף את פסק הדין של reCAPTCHA Enterprise עבור פעולת המשתמש

הדוגמה הבאה מראה כיצד לעקוף פסק דין של reCAPTCHA Enterprise עבור זרימות משתמשים נתמכות.

עיין ב- Enable reCAPTCHA Enterprise כדי ללמוד עוד על שילוב reCAPTCHA Enterprise עם אימות Firebase.

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

Node.js

 const {
   auth,
 } = require("firebase-functions/v1");

exports.checkrecaptchaV1 = auth.user().beforeSignIn((userRecord, context) => {
 // Allow users with a specific email domain to sign in regardless of their recaptcha score.
 if (userRecord.email && userRecord.email.indexOf('@acme.com') === -1) {
   return {
     recaptchaActionOverride: 'ALLOW',
   };
 }

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

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