Authentication のブロック トリガー

Identity Platform を使用する Firebase Authentication にアップグレードした場合は、ブロッキング Cloud Functions を使用して Firebase Authentication を拡張できます。

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

始める前に

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

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

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

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

  • beforeUserSignedIn: ユーザーの認証情報が検証されてから、Firebase Authentication からクライアント アプリに ID トークンが返らないうちにトリガーされます。アプリで多要素認証を使用する場合、この関数は、ユーザーが第 2 要素を検証した後にトリガーされます。新しいユーザーを作成すると、beforeUserCreated だけでなく beforeUserSignedIn もトリガーされます。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    firebase deploy --only functions
    

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

ブロッキング関数の登録

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

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

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

  4. 変更を保存します。

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

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

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

次の表に、発生する可能性があるエラーと、デフォルトのエラー メッセージを示します。

名前 コード メッセージ
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 HttpsError('permission-denied', '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");
  }
});

デフォルト メッセージとカスタム メッセージのどちらを使用する場合でも、Cloud Functions でエラーがラップされ、内部エラーとしてクライアントに返されます。例:

throw new HttpsError('invalid-argument', "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';
  };
});

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 はい はい 使用不可 はい

更新トークン

ブロッキング関数で更新トークンを使用するには、まず 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');
  }
});

未確認のアドレスを使用するユーザーを登録からブロックする

次の例は、未確認のアドレスを使用しているユーザーがアプリに登録できないようにする方法を示しています。

Node.js

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

登録時にメール確認を必須にする

次の例は、登録後にユーザーにメールアドレスの確認を求める方法を示しています。

Node.js

export const beforecreated = beforeUserCreated((event) => {
  const user = event.data;
  const locale = event.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);
    });
  }
});

export const beforesignedin = beforeUserSignedIn((event) => {
 const user = event.data;
 if (user.email && !user.emailVerified) {
   throw new HttpsError(
     'invalid-argument', 'The email needs to be verified before access is granted.');
  }
});

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

特定の IP アドレスからのログインをブロックする

次の例は、特定の IP アドレス範囲からのログインをブロックする方法を示しています。

Node.js

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

カスタム クレームとセッション クレームの設定

次の例は、カスタム クレームとセッション クレームを設定する方法を示しています。

Node.js

export const beforecreated = beforeUserCreated((event) => {
  if (event.credential &&
      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,
      },
      // Copy role and groups to token claims. These will not be persisted.
      sessionClaims: {
        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,
        },
      };
    });
    
  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

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

画像を検出してサニタイズする方法について詳しくは、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();
          });
      });
    })
  }
});