Firebase Summit で発表されたすべての情報をご覧ください。Firebase を使用してアプリ開発を加速し、自信を持ってアプリを実行する方法を紹介しています。詳細

クラウド機能をブロックしてFirebase認証を拡張する

コレクションでコンテンツを整理 必要に応じて、コンテンツの保存と分類を行います。

IdentityPlatformを使用したFirebaseAuthenticationにアップグレードした場合は、クラウド機能のブロックを使用してFirebaseAuthenticationを拡張できます。

ブロック機能を使用すると、ユーザーがアプリに登録またはサインインした結果を変更するカスタムコードを実行できます。たとえば、ユーザーが特定の基準を満たしていない場合にユーザーが認証されないようにしたり、クライアントアプリに返す前にユーザーの情報を更新したりできます。

あなたが始める前に

ブロッキング機能を使用するには、FirebaseプロジェクトをIdentityPlatformを使用したFirebaseAuthenticationにアップグレードする必要があります。まだアップグレードしていない場合は、最初にアップグレードしてください。

ブロッキング機能を理解する

次の2つのイベントのブロック機能を登録できます。

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

  • beforeSignIn :ユーザーの認証情報が確認された後、FirebaseAuthenticationがクライアントアプリにIDトークンを返す前にトリガーします。アプリが多要素認証を使用している場合、ユーザーが2番目の要素を確認した後に関数がトリガーされます。新しいユーザーを作成すると、 beforeSignInに加えて、 beforeCreateもトリガーされることに注意してください。

ブロッキング機能を使用するときは、次の点に注意してください。

  • 関数は7秒以内に応答する必要があります。 7秒後、Firebase Authenticationはエラーを返し、クライアントの操作は失敗します。

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

  • 関数は、テナントに含まれるすべてのユーザーを含む、プロジェクト内のすべてのユーザーに適用されます。 Firebase Authenticationは、ユーザーが所属するテナントなど、ユーザーに関する情報を機能に提供するため、それに応じて対応できます。

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

  • 匿名およびカスタム認証は、ブロッキング機能をトリガーしません。

ブロッキング機能をデプロイして登録する

カスタムコードをユーザー認証フローに挿入するには、ブロック機能をデプロイして登録します。ブロック機能をデプロイして登録したら、認証とユーザー作成を成功させるには、カスタムコードが正常に完了する必要があります。

ブロッキング機能をデプロイする

ブロッキング関数は、他の関数をデプロイするのと同じ方法でデプロイします。 (詳細については、Cloud Functionsの開始ページを参照してください)。要約すれば:

  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. FirebaseCLIを使用して関数をデプロイします。

    firebase deploy --only functions
    

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

ブロッキング機能を登録する

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

  2. [ブロック機能]タブを選択します。

  3. アカウント作成前(beforeCreate)またはサインイン前(beforeSignIn)のいずれかの下にあるドロップダウンメニューからブロック機能を選択して、ブロック機能を登録します。

  4. 変更を保存します。

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

beforeSignInイベントとbeforeCreateイベントは、ユーザーのサインインに関する情報を含むUserオブジェクトとEventContextオブジェクトを提供します。コードでこれらの値を使用して、操作の続行を許可するかどうかを決定します。

Userオブジェクトで使用可能なプロパティのリストについては、 UserRecordリファレンスを参照してください。

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イベントタイプ。これにより、 beforeSignInbeforeCreateなどのイベント名と、Googleや電子メール/パスワードなどの関連するサインイン方法に関する情報が提供されます。 providers/cloud.auth/eventTypes/user.beforeSignIn:password
authType常にUSER USER
resource FirebaseAuthenticationプロジェクトまたはテナント。 projects/ project-id /tenants/ tenant-id
timestampイベントがトリガーされた時刻。RFC3339文字列としてフォーマットされています。 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.
    }
  });

ユーザーの変更

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

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

  • displayName
  • disabled
  • emailVerified
  • photoURL
  • customClaims
  • sessionClaimsbeforeSignInのみ)

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

beforeCreatebeforeSignInの両方にイベントハンドラーを登録する場合、 beforeCreatebeforeSignInの後に実行されることに注意してください。 beforeCreateで更新されたユーザーフィールドは、 beforeCreateに表示されbeforeSignIn 。両方のイベントハンドラーでsessionClaims以外のフィールドを設定した場合、 beforeCreateで設定された値は、 beforeSignInで設定された値を上書きします。 sessionClaimsの場合のみ、現在のセッションのトークンクレームに伝播されますが、データベースに永続化または保存されることはありません。

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

サポートされているOAuthクレデンシャルとデータ

OAuthクレデンシャルとデータを、さまざまなIDプロバイダーのブロック機能に渡すことができます。次の表は、各IDプロバイダーでサポートされている資格情報とデータを示しています。

IDプロバイダーIDトークンアクセストークン有効期限トークンシークレットトークンを更新サインインクレーム
グーグルはいはいはいいいえはいいいえ
フェイスブックいいえはいはいいいえいいえいいえ
ツイッターいいえはいいいえはいいいえいいえ
GitHubいいえはいいいえいいえいいえいいえ
マイクロソフトはいはいはいいいえはいいいえ
LinkedInいいえはいはいいいえいいえいいえ
Yahooはいはいはいいいえはいいいえ
アップルはいはいはいいいえはいいいえ
SAMLいいえいいえいいえいいえいいえはい
OIDCはいはいはいいいえはいはい

トークンを更新する

ブロック機能で更新トークンを使用するには、最初にFirebaseコンソールの[ブロック機能]ページのチェックボックスを選択する必要があります。

IDトークンやアクセストークンなどのOAuthクレデンシャルを使用して直接サインインする場合、IDプロバイダーから更新トークンが返されることはありません。この状況では、同じクライアント側のOAuthクレデンシャルがブロッキング関数に渡されます。

次のセクションでは、各IDプロバイダーの種類と、サポートされている資格情報およびデータについて説明します。

一般的なOIDCプロバイダー

ユーザーが汎用OIDCプロバイダーでサインインすると、次の資格情報が渡されます。

  • IDトークンid_tokenフローが選択されている場合に提供されます。
  • アクセストークン:コードフローが選択されている場合に提供されます。コードフローは現在、RESTAPIを介してのみサポートされていることに注意してください。
  • トークンの更新offline_accessスコープが選択されている場合に提供されます。

例:

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

グーグル

ユーザーがGoogleにサインインすると、次のクレデンシャルが渡されます。

  • IDトークン
  • アクセストークン
  • トークンの更新:次のカスタムパラメーターが要求された場合にのみ提供されます。
    • 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

ユーザーがGitHubでサインインすると、次のクレデンシャルが渡されます。

  • アクセストークン:取り消されない限り、有効期限はありません。

マイクロソフト

ユーザーがMicrosoftにサインインすると、次の資格情報が渡されます。

  • IDトークン
  • アクセストークン
  • トークンの更新offline_accessスコープが選択されている場合、ブロッキング関数に渡されます。

例:

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

Yahoo

ユーザーがYahooでサインインすると、カスタムパラメーターやスコープなしで次の資格情報が渡されます。

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

LinkedIn

ユーザーがLinkedInでサインインすると、次のクレデンシャルが渡されます。

  • アクセストークン

アップル

ユーザーがAppleにサインインすると、カスタムパラメータやスコープなしで次のクレデンシャルが渡されます。

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

一般的なシナリオ

次の例は、関数をブロックするためのいくつかの一般的な使用例を示しています。

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

次の例は、 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.`);
  }
});

特定のIDプロバイダーの電子メールを検証済みとして扱う

次の例は、特定のIDプロバイダーからのユーザーの電子メールを検証済みとして処理する方法を示しています。

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. ユーザーがFirebaseAuthenticationによる認証を必要とするリソースにアクセスしようとした場合、リクエストの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,
          };
        }
      });
});

画像を検出してサニタイズする方法の詳細については、 CloudVisionのドキュメントをご覧ください。

ユーザーのIDプロバイダーのOAuthクレデンシャルへのアクセス

次の例は、Googleでログインしたユーザーの更新トークンを取得し、それを使用してGoogleカレンダー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
);

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