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 のスタートガイドをご覧ください)。要約:
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 });
上記の例では、カスタム認証ロジックの実装は省略されています。固有の例に関するブロッキング関数の実装方法と一般的な使用方法については、次のセクションをご覧ください。
Firebase CLI を使用して次のように関数をデプロイします。
firebase deploy --only functions
関数を更新するたびに再デプロイする必要があります。
ブロッキング関数の登録
Firebase コンソールの Firebase Authentication の設定ページに移動します。
[ブロッキング関数] タブを選択します。
ブロッキング関数を登録するには、[アカウント作成前(beforeCreate)] または [ログイン前(beforeSignIn)] にあるプルダウン メニューからブロッキング関数を選択します。
変更を保存します。
ユーザー情報とコンテキスト情報の取得
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
|
イベントタイプ。これにより、イベント名(beforeSignIn 、beforeCreate など)と使用する関連付けられているログイン方法(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
sessionClaims
(beforeUserSignedIn
のみ)
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';
};
});
beforeUserCreated
と beforeUserSignedIn
の両方にイベント ハンドラを登録した場合、beforeUserSignedIn
は beforeUserCreated
の後に実行されます。beforeUserCreated
で更新されたユーザー フィールドは beforeUserSignedIn
に表示されます。両方のイベント ハンドラに sessionClaims
以外のフィールドを設定すると、beforeUserSignedIn
に設定された値で beforeUserCreated
に設定された値が上書きされます。sessionClaims
の場合のみ、現在のセッションのトークン クレームに伝播されますが、データベースには保持または格納されません。
たとえば、sessionClaims
が設定されている場合、beforeUserSignedIn
は任意の beforeUserCreated
クレームとともにそれらを返し、それらがマージされます。マージ時に、sessionClaims
キーが customClaims
のキーと一致する場合、一致する customClaims
は sessionClaims
キーによってトークン クレームで上書きされます。ただし、上書きされた customClaims
キーは、今後のリクエストに備えてデータベースに保持されます。
サポートされている OAuth 認証情報とデータ
OAuth 認証情報とデータを渡して、さまざまな ID プロバイダから関数をブロックできます。次の表は、各 ID プロバイダでサポートされる認証情報とデータをまとめたものです。
ID プロバイダ | ID トークン | アクセス トークン | 有効期限 | トークン シークレット | 更新トークン | ログイン クレーム |
---|---|---|---|---|---|---|
○ | はい | はい | 使用不可 | 使用可 | × | |
× | はい | はい | 使用不可 | 使用不可 | × | |
× | 使用可 | 使用不可 | 使用可 | 使用不可 | × | |
GitHub | × | 使用可 | 使用不可 | 使用不可 | 使用不可 | × |
Microsoft | ○ | はい | はい | 使用不可 | 使用可 | × |
× | はい | はい | 使用不可 | 使用不可 | × | |
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 でログインすると、次の認証情報が渡されます。
- 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 でサポートされているさまざまなアクセス トークンと、それらのトークンを長期間有効なトークンに交換する方法をご覧ください。
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 でログインすると、次の認証情報が渡されます。
- アクセス トークン
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 の地理的リージョンが異なるなど)は、ユーザーに再度ログインするよう求めることができます。
セッション クレームを使用して、ユーザーがログインした IP アドレスを追跡します。
Node.js
export const beforesignedin = beforeUserSignedIn((event) => { return { sessionClaims: { signInIpAddress: event.ipAddress, }, }; });
ユーザーが 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();
});
});
})
}
});