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の開始ページを参照してください)。要約すれば:
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 });
上記の例では、カスタム認証ロジックの実装を省略しています。特定の例については、ブロッキング関数と一般的なシナリオを実装する方法について、次のセクションを参照してください。
FirebaseCLIを使用して関数をデプロイします。
firebase deploy --only functions
関数を更新するたびに、関数を再デプロイする必要があります。
ブロッキング機能を登録する
Firebaseコンソールの[Firebase認証設定]ページに移動します。
[ブロック機能]タブを選択します。
アカウント作成前(beforeCreate)またはサインイン前(beforeSignIn)のいずれかの下にあるドロップダウンメニューからブロック機能を選択して、ブロック機能を登録します。
変更を保存します。
ユーザーとコンテキスト情報の取得
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 | イベントタイプ。これにより、 beforeSignIn やbeforeCreate などのイベント名と、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
-
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
の両方にイベントハンドラーを登録する場合、 beforeCreate
はbeforeSignIn
の後に実行されることに注意してください。 beforeCreateで更新されたユーザーフィールドは、 beforeCreate
に表示されbeforeSignIn
。両方のイベントハンドラーでsessionClaims
以外のフィールドを設定した場合、 beforeCreate
で設定された値は、 beforeSignIn
で設定された値を上書きします。 sessionClaims
の場合のみ、現在のセッションのトークンクレームに伝播されますが、データベースに永続化または保存されることはありません。
たとえば、 sessionClaims
が設定されている場合、 beforeSignIn
はbeforeCreate
クレームとともにそれらを返し、それらはマージされます。それらがマージされるときに、 sessionClaims
キーがcustomClaimsのキーと一致する場合、一致するcustomClaims
は、 customClaims
キーによってトークンクレームで上書きさsessionClaims
ます。ただし、上書きされたcustomClaims
キーは、今後のリクエストのためにデータベースに保持されます。
サポートされているOAuthクレデンシャルとデータ
OAuthクレデンシャルとデータを、さまざまなIDプロバイダーのブロック機能に渡すことができます。次の表は、各IDプロバイダーでサポートされている資格情報とデータを示しています。
IDプロバイダー | IDトークン | アクセストークン | 有効期限 | トークンシークレット | トークンを更新 | サインインクレーム |
---|---|---|---|---|---|---|
グーグル | はい | はい | はい | いいえ | はい | いいえ |
フェイスブック | いいえ | はい | はい | いいえ | いいえ | いいえ |
ツイッター | いいえ | はい | いいえ | はい | いいえ | いいえ |
GitHub | いいえ | はい | いいえ | いいえ | いいえ | いいえ |
マイクロソフト | はい | はい | はい | いいえ | はい | いいえ |
いいえ | はい | はい | いいえ | いいえ | いいえ | |
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でサインインすると、次のクレデンシャルが渡されます。
- アクセストークン
アップル
ユーザーが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が異なる地理的地域からのものである場合)、ユーザーに再度サインインするように依頼できます。
セッションクレームを使用して、ユーザーがサインインするIPアドレスを追跡します。
Node.js
exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => { return { sessionClaims: { signInIpAddress: context.ipAddress, }, }; });
ユーザーが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();
});
});
})
}
});