ブロッキング Cloud Functions を使用して Firebase Authentication を拡張する


ブロッキング関数を使用することで、アプリの登録やログインの結果を変更するカスタムコードを実行できます。たとえば、ユーザーが特定の条件を満たしていない場合にユーザーが認証されないようにすることや、クライアント アプリに戻る前にユーザーの情報を更新できます。

始める前に

ブロッキング関数を使用するには、Firebase プロジェクトを Identity Platform を使用する Firebase Authentication にアップグレードする必要があります。まだアップグレードしていない場合は、事前にアップグレードしてください。

ブロッキング関数について

ブロッキング関数は、次の 2 つのイベントで登録できます。

  • ユーザーが作成される前: 新しいユーザーが Firebase Authentication データベースに保存される前と、トークンがクライアント アプリに返される前にトリガーします。

  • ユーザーがログインする前: ユーザーの認証情報が検証された後、Firebase Authentication がクライアント アプリに ID トークンを返す前にトリガーされます。アプリで多要素認証を使用する場合、ユーザーが第 2 要素を認証すると関数がトリガーされます。新しいユーザーを作成すると、これらのイベントもトリガーされます。

ブロッキング関数を使用する際は、次の点に注意してください。

  • 関数は 7 秒以内に応答する必要があります。7 秒経過すると、Firebase Authentication がエラーを返し、クライアント オペレーションが失敗します。

  • 200 以外の HTTP レスポンス コードがクライアント アプリに渡されます。関数から返される可能性のあるエラーをクライアント コードが処理していることを確認します。

  • 関数は、テナント内にあるものを含む、プロジェクト内のすべてのユーザーに適用されます。Firebase Authentication では、ユーザーに関する情報(ユーザーが所属しているテナントを含む)が関数に示されるため、それに従って適切に対処できます。

  • 別の ID プロバイダをアカウントにリンクすると、登録済みの beforeUserSignedIn 関数が再トリガーされます。

  • 匿名認証とカスタム認証では、ブロッキング関数はトリガーされません。

ブロッキング関数のデプロイと登録

カスタムコードをユーザー認証フローに挿入するには、ブロッキング関数をデプロイして登録します。ブロッキング関数をデプロイおよび登録後は、カスタムコードで認証が正常に完了し、ユーザーの作成が成功する必要があります。

ブロッキング関数のデプロイ

ブロッキング関数も、他の関数と同じ方法でデプロイできます(詳細については、Cloud Functions のスタートガイドをご覧ください)。要約:

  1. beforeUserCreated イベント、beforeUserSignedIn イベント、またはその両方を処理する Cloud Functions を作成します。

    たとえば、最初に次の no-op 関数を index.js に追加します。

    Node.js

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

    Python(プレビュー)

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

    上記の例では、カスタム認証ロジックの実装は省略されています。固有の例に関するブロッキング関数の実装方法と一般的な使用方法については、次のセクションをご覧ください。

  2. Firebase CLI を使用して次のように関数をデプロイします。

    firebase deploy --only functions
    

    関数を更新するたびに再デプロイする必要があります。

ブロッキング関数の登録

  1. Firebase コンソールの Firebase Authentication の設定ページに移動します。

  2. [ブロッキング関数] タブを選択します。

  3. ブロッキング関数を登録するには、[アカウント作成前(beforeCreate)] または [ログイン前(beforeSignIn)] にあるプルダウン メニューからブロッキング関数を選択します。

  4. 変更を保存します。

ユーザー情報とコンテキスト情報の取得

beforeUserSignedIn イベントと beforeUserCreated イベントは、ユーザーのログインに関する情報を含む AuthBlockingEvent オブジェクトを提供します。コード内で、これらの値を使用して、オペレーションの続行を許可するかどうかを決定します。

ブロッキング イベントは、ユーザーのログインに関する情報を含むオブジェクトを提供します。コード内で、これらの値を使用して、オペレーションの続行を許可するかどうかを決定します。

オブジェクトには次のプロパティが含まれています。

名前 説明
locale アプリの言語 / 地域。言語 / 地域を設定するには、クライアント SDK を使用するか、REST API で言語 / 地域のヘッダーを渡します。 fr または sv-SE
ipAddress エンドユーザーが登録またはログインに使用したデバイスの IP アドレス。 114.14.200.1
userAgent ブロック関数をトリガーするユーザー エージェント。 Mozilla/5.0 (X11; Linux x86_64)
eventId イベントの一意の ID。 rWsyPtolplG2TBFoOkkgyg
eventType イベントタイプ。これにより、イベント名(beforeSignInbeforeCreate など)と使用する関連付けられているログイン方法(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.
    }
}

ユーザーの変更

登録またはログインの試行をブロックする代わりに、この操作の続行を許可できますが、Firebase Authentication のデータベースに保存される User オブジェクトは変更され、クライアントに返されます。

ユーザーを変更するには、変更するフィールドを含むイベント ハンドラからオブジェクトを返します。次のフィールドを変更できます。

  • displayName
  • disabled
  • emailVerified
  • photoUrl
  • customClaims
  • sessionClaimsbeforeUserSignedIn のみ)

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


beforeUserCreatedbeforeUserSignedIn の両方にイベント ハンドラを登録した場合、beforeUserSignedInbeforeUserCreated の後に実行されます。beforeUserCreated で更新されたユーザー フィールドは beforeUserSignedIn に表示されます。両方のイベント ハンドラに sessionClaims 以外のフィールドを設定すると、beforeUserSignedIn に設定された値で beforeUserCreated に設定された値が上書きされます。sessionClaims の場合のみ、現在のセッションのトークン クレームに伝播されますが、データベースには保持または格納されません。

たとえば、sessionClaims が設定されている場合、beforeUserSignedIn は任意の beforeUserCreated クレームとともにそれらを返し、それらがマージされます。マージ時に、sessionClaims キーが customClaims のキーと一致する場合、一致する customClaimssessionClaims キーによってトークン クレームで上書きされます。ただし、上書きされた customClaims キーは、今後のリクエストに備えてデータベースに保持されます。

サポートされている OAuth 認証情報とデータ

OAuth 認証情報とデータを渡して、さまざまな ID プロバイダから関数をブロックできます。次の表は、各 ID プロバイダでサポートされる認証情報とデータをまとめたものです。

ID プロバイダ ID トークン アクセス トークン 有効期限 トークン シークレット 更新トークン ログイン クレーム
Google はい × ×
Facebook × はい いいえ いいえ ×
Twitter × × いいえ ×
GitHub × いいえ いいえ いいえ ×
Microsoft はい × ×
LinkedIn × はい いいえ いいえ ×
Yahoo! はい × ×
Apple はい × ×
SAML × いいえ いいえ いいえ いいえ
OIDC はい ×

OAuth トークン

ブロッキング関数で ID トークン、アクセス トークン、または更新トークンを使用するには、事前に Firebase コンソールの [ブロッキング関数] ページでチェックボックスをオンにしておく必要があります。

ID トークンやアクセス トークンなどの OAuth 認証情報を使用して直接ログインする場合、ID プロバイダから更新トークンは返されません。この場合、同じクライアント側の OAuth 認証情報がブロッキング関数に渡されます。

以降のセクションで、各 ID プロバイダの種類とサポートされている認証情報とデータについて説明します。

一般的な OIDC プロバイダ

ユーザーが一般的な OIDC プロバイダを使用してログインすると、次の認証情報が渡されます。

  • ID トークン: 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 でログインすると、次の認証情報が渡されます。

  • ID トークン
  • アクセス トークン
  • 更新トークン: 次のカスタム パラメータがリクエストされた場合にのみ提供されます。
    • 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 でログインすると、次の認証情報が渡されます。

  • ID トークン
  • アクセス トークン
  • 更新トークン: 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 でログインすると、次の認証情報がカスタム パラメータやスコープなしで渡されます。

  • ID トークン
  • アクセス トークン
  • 更新トークン

LinkedIn

ユーザーが LinkedIn でログインすると、次の認証情報が渡されます。

  • アクセス トークン

Apple

ユーザーが Apple でログインすると、次の認証情報がカスタム パラメータやスコープなしで渡されます。

  • ID トークン
  • アクセス トークン
  • 更新トークン

一般的な使用方法

次の例は、関数をブロックする一般的なユースケースを示しています。

特定のドメインからの登録のみを許可する

次の例は、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.",
        )


特定の ID プロバイダのメールアドレスを確認済みとして処理する

次の例は、特定の ID プロバイダからのユーザーのメールアドレスを確認済みとみなす方法を示しています。

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 のドキュメントをご覧ください。

ユーザーの ID プロバイダの OAuth 認証情報にアクセスする

次の例は、Google でログインしたユーザーの更新トークンを取得し、それを使用して Google Calendar API を呼び出す方法を示しています。更新トークンは、オフライン アクセスのために保存されます。

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)