Join us in person and online for Firebase Summit on October 18, 2022. Learn how Firebase can help you accelerate app development, release your app with confidence, and scale with ease. Register now

قم بتوسيع مصادقة Firebase مع حظر وظائف السحابة

تنظيم صفحاتك في مجموعات يمكنك حفظ المحتوى وتصنيفه حسب إعداداتك المفضّلة.

إذا قمت بالترقية إلى Firebase Authentication باستخدام Identity Platform ، فيمكنك تمديد مصادقة Firebase باستخدام حظر وظائف السحابة .

تتيح لك وظائف الحظر تنفيذ تعليمات برمجية مخصصة تعدل نتيجة تسجيل المستخدم أو تسجيل الدخول إلى تطبيقك. على سبيل المثال ، يمكنك منع المستخدم من المصادقة إذا لم يستوف معايير معينة ، أو تحديث معلومات المستخدم قبل إعادتها إلى تطبيق العميل الخاص بك.

قبل ان تبدأ

لاستخدام وظائف الحظر ، يجب ترقية مشروع Firebase إلى Firebase Authentication مع Identity Platform. إذا لم تكن قد قمت بالترقية بالفعل ، فقم بذلك أولاً.

فهم وظائف الحجب

يمكنك تسجيل وظائف الحظر لحدثين:

  • beforeCreate : المشغلات قبل حفظ مستخدم جديد في قاعدة بيانات Firebase Authentication ، وقبل إرجاع رمز مميز إلى تطبيق العميل.

  • beforeSignIn : المشغلات بعد التحقق من بيانات اعتماد المستخدم ، ولكن قبل أن تعيد مصادقة Firebase رمزًا مميزًا للمعرف إلى تطبيق العميل الخاص بك. إذا كان تطبيقك يستخدم مصادقة متعددة العوامل ، يتم تشغيل الوظيفة بعد أن يتحقق المستخدم من العامل الثاني. لاحظ أن إنشاء مستخدم جديد يتم تشغيله أيضًا beforeSignIn ، بالإضافة إلى beforeCreate .

ضع في اعتبارك ما يلي عند استخدام وظائف الحظر:

  • يجب أن تستجيب وظيفتك في غضون 7 ثوانٍ. بعد 7 ثوانٍ ، تُرجع مصادقة Firebase خطأً ، وتفشل عملية العميل.

  • يتم تمرير رموز استجابة HTTP بخلاف 200 إلى تطبيقات العميل. تأكد من أن رمز العميل الخاص بك يعالج أي أخطاء يمكن أن ترجعها وظيفتك.

  • تنطبق الوظائف على جميع المستخدمين في مشروعك ، بما في ذلك أي وظائف موجودة في المستأجر . توفر مصادقة Firebase معلومات حول المستخدمين لوظيفتك ، بما في ذلك أي مستأجرين ينتمون إليهم ، حتى تتمكن من الاستجابة وفقًا لذلك.

  • يؤدي ربط موفر هوية آخر بحساب ما إلى إعادة تشغيل أي تسجيل تم تسجيله قبل وظائف beforeSignIn .

  • لا تؤدي المصادقة المجهولة والمخصصة إلى تشغيل وظائف الحظر.

نشر وتسجيل وظيفة الحظر

لإدراج رمزك المخصص في تدفقات مصادقة المستخدم ، قم بنشر وظائف الحظر وتسجيلها. بمجرد نشر وظائف الحظر الخاصة بك وتسجيلها ، يجب أن يكتمل الرمز المخصص بنجاح للمصادقة وإنشاء المستخدم.

نشر وظيفة الحظر

تقوم بنشر وظيفة الحظر بنفس طريقة نشر أي وظيفة. (راجع صفحة بدء وظائف السحابة للحصول على التفاصيل). في تلخيص:

  1. اكتب وظائف السحابة التي تتعامل مع الحدث beforeCreate أو الحدث beforeSignIn أو كليهما.

    على سبيل المثال ، للبدء ، يمكنك إضافة وظائف no-op التالية إلى 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
    

    يجب إعادة نشر وظائفك في كل مرة تقوم فيها بتحديثها.

تسجيل وظيفة الحجب

  1. انتقل إلى صفحة إعدادات مصادقة Firebase في وحدة تحكم Firebase.

  2. حدد علامة التبويب وظائف الحظر .

  3. قم بتسجيل وظيفة الحظر الخاصة بك عن طريق تحديدها من القائمة المنسدلة ضمن إما قبل إنشاء الحساب (قبل إنشاء) أو قبل تسجيل الدخول (قبل تسجيل الدخول) .

  4. احفظ التغييرات.

الحصول على معلومات المستخدم والسياق

يوفر 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}"`);
  }
});

بغض النظر عما إذا كنت تستخدم رسالة افتراضية أو مخصصة ، فإن وظائف السحابة تغلف الخطأ وتعيده إلى العميل كخطأ داخلي. فمثلا:

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

يجب أن يكتشف تطبيقك الخطأ ويتعامل معه وفقًا لذلك. فمثلا:

جافا سكريبت

// 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 في كلا معالجي الأحداث ، فستستبدل القيمة التي تم تعيينها beforeCreate beforeSignIn بالنسبة إلى sessionClaims فقط ، يتم نشرها إلى مطالبات الرمز المميز للجلسة الحالية ، ولكن لا يتم استمرارها أو تخزينها في قاعدة البيانات.

على سبيل المثال ، إذا تم تعيين أي مطالبات جلسة ، sessionClaims beforeSignIn مع أي مطالبات beforeCreate ، وسيتم دمجها. عند دمجهم ، إذا تطابق مفتاح sessionClaims مع مفتاح في customClaims ، فسيتم استبدال customClaims المطابقة في مطالبات الرمز المميز بواسطة مفتاح sessionClaims . ومع ذلك ، فإن مفتاح customClaims المكتوب فوقه سيظل موجودًا في قاعدة البيانات للطلبات المستقبلية.

بيانات اعتماد وبيانات OAuth المدعومة

يمكنك تمرير بيانات اعتماد وبيانات OAuth إلى وظائف الحظر من موفري الهوية المختلفين. يوضح الجدول التالي بيانات الاعتماد والبيانات المدعومة لكل موفر هوية:

مزود الهوية رمز الهوية رمز وصول تاريخ انتهاء الصلاحية سر الرمز المميز تحديث الرمز مطالبات تسجيل الدخول
جوجل نعم نعم نعم رقم نعم رقم
فيسبوك رقم نعم نعم رقم رقم رقم
تويتر رقم نعم رقم نعم رقم رقم
جيثب رقم نعم رقم رقم رقم رقم
مايكروسوفت نعم نعم نعم رقم نعم رقم
ينكدين رقم نعم نعم رقم رقم رقم
ياهو نعم نعم نعم رقم نعم رقم
تفاحة نعم نعم نعم رقم نعم رقم
SAML رقم رقم رقم رقم رقم نعم
OIDC نعم نعم نعم رقم نعم نعم

تحديث الرموز

لاستخدام رمز التحديث المميز في وظيفة الحظر ، يجب عليك أولاً تحديد مربع الاختيار في صفحة وظائف الحظر بوحدة تحكم Firebase.

لن يتم إرجاع الرموز المميزة للتحديث بواسطة أي من موفري الهوية عند تسجيل الدخول مباشرةً باستخدام بيانات اعتماد OAuth ، مثل الرمز المميز للمعرف أو رمز الوصول. في هذه الحالة ، سيتم تمرير نفس بيانات اعتماد OAuth من جانب العميل إلى وظيفة الحظر.

تصف الأقسام التالية كل أنواع موفري الهوية وبيانات اعتمادهم وبياناتهم المدعومة.

موفرو OIDC العام

عندما يقوم المستخدم بتسجيل الدخول باستخدام موفر OIDC عام ، سيتم تمرير بيانات الاعتماد التالية:

  • رمز المعرف المميز : يتم توفيره إذا تم تحديد تدفق id_token .
  • رمز الوصول : يتم توفيره إذا تم تحديد تدفق التعليمات البرمجية. لاحظ أن تدفق الشفرة مدعوم حاليًا فقط عبر واجهة برمجة تطبيقات REST.
  • تحديث الرمز المميز : يتم توفيره إذا تم 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 .

فيسبوك

عندما يقوم المستخدم بتسجيل الدخول باستخدام Facebook ، سيتم تمرير بيانات الاعتماد التالية:

  • رمز الوصول : يتم إرجاع رمز وصول يمكن استبداله برمز وصول آخر. تعرف على المزيد حول الأنواع المختلفة لرموز الوصول التي يدعمها Facebook وكيف يمكنك استبدالها برموز طويلة العمر .

جيثب

عندما يقوم المستخدم بتسجيل الدخول باستخدام GitHub ، سيتم تمرير بيانات الاعتماد التالية:

  • رمز الوصول : لا تنتهي صلاحيته ما لم يتم إبطاله.

مايكروسوفت

عندما يقوم المستخدم بتسجيل الدخول مع Microsoft ، سيتم تمرير بيانات الاعتماد التالية:

مثال:

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 ، قارن عنوان 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 ، واستخدامه لاستدعاء واجهات برمجة تطبيقات تقويم 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();
          });
      });
    })
  }
});