バニラJavaScriptでFirebaseアプリケーションを書いています。 Firebase AuthenticationとFirebaseUI for Webを使用しています。 Firebase Cloud Functionsを使用して、ページルートのリクエストを受信し、レンダリングされたHTMLを返すサーバーを実装しています。クライアント側で認証済みIDトークンを利用して、Firebase Cloud Functionが提供する保護されたルートにアクセスするためのベストプラクティスを見つけるのに苦労しています。
基本的なフローを理解していると思います。ユーザーがログインすると、IDトークンがクライアントに送信され、onAuthStateChanged
コールバックで受信され、Authorization
フィールドに挿入されます。適切なプレフィックスが付いた新しいHTTPリクエスト。ユーザーが保護されたルートにアクセスしようとすると、サーバーによってチェックされます。
onAuthStateChanged
コールバック内のIDトークンをどうするか、または必要に応じてクライアント側JavaScriptを変更してリクエストヘッダーを変更する方法がわかりません。
Firebase Cloud Functionsを使用してルーティングリクエストを処理しています。これが私のfunctions/index.js
は、すべてのリクエストがリダイレクトされ、IDトークンがチェックされるapp
メソッドをエクスポートします。
const functions = require('firebase-functions')
const admin = require('firebase-admin')
const express = require('express')
const cookieParser = require('cookie-parser')
const cors = require('cors')
const app = express()
app.use(cors({ Origin: true }))
app.use(cookieParser())
admin.initializeApp(functions.config().firebase)
const firebaseAuthenticate = (req, res, next) => {
console.log('Check if request is authorized with Firebase ID token')
if ((!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) &&
!req.cookies.__session) {
console.error('No Firebase ID token was passed as a Bearer token in the Authorization header.',
'Make sure you authorize your request by providing the following HTTP header:',
'Authorization: Bearer <Firebase ID Token>',
'or by passing a "__session" cookie.')
res.status(403).send('Unauthorized')
return
}
let idToken
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
console.log('Found "Authorization" header')
// Read the ID Token from the Authorization header.
idToken = req.headers.authorization.split('Bearer ')[1]
} else {
console.log('Found "__session" cookie')
// Read the ID Token from cookie.
idToken = req.cookies.__session
}
admin.auth().verifyIdToken(idToken).then(decodedIdToken => {
console.log('ID Token correctly decoded', decodedIdToken)
console.log('token details:', JSON.stringify(decodedIdToken))
console.log('User email:', decodedIdToken.firebase.identities['google.com'][0])
req.user = decodedIdToken
return next()
}).catch(error => {
console.error('Error while verifying Firebase ID token:', error)
res.status(403).send('Unauthorized')
})
}
const meta = `<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/2.6.0/firebaseui.css" />
const logic = `<!-- Intialization -->
<script src="https://www.gstatic.com/firebasejs/4.10.0/firebase.js"></script>
<script src="/init.js"></script>
<!-- Authentication -->
<script src="https://cdn.firebase.com/libs/firebaseui/2.6.0/firebaseui.js"></script>
<script src="/auth.js"></script>`
app.get('/', (request, response) => {
response.send(`<html>
<head>
<title>Index</title>
${meta}
</head>
<body>
<h1>Index</h1>
<a href="/user/fake">Fake User</a>
<div id="firebaseui-auth-container"></div>
${logic}
</body>
</html>`)
})
app.get('/user/:name', firebaseAuthenticate, (request, response) => {
response.send(`<html>
<head>
<title>User - ${request.params.name}</title>
${meta}
</head>
<body>
<h1>User ${request.params.name}</h1>
${logic}
</body>
</html>`)
})
exports.app = functions.https.onRequest(app)
彼女は私のfunctions/package.json
は、Firebase Cloud Functionとして実装されたHTTPリクエストを処理するサーバーの構成を示しています。
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"lint": "./node_modules/.bin/eslint .",
"serve": "firebase serve --only functions",
"Shell": "firebase experimental:functions:Shell",
"start": "npm run Shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"dependencies": {
"cookie-parser": "^1.4.3",
"cors": "^2.8.4",
"eslint-config-standard": "^11.0.0-beta.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^6.0.0",
"eslint-plugin-standard": "^3.0.1",
"firebase-admin": "~5.8.1",
"firebase-functions": "^0.8.1"
},
"devDependencies": {
"eslint": "^4.12.0",
"eslint-plugin-promise": "^3.6.0"
},
"private": true
}
これが私のfirebase.json
、すべてのページリクエストをエクスポートされたapp
関数にリダイレクトします。
{
"functions": {
"predeploy": [
"npm --prefix $RESOURCE_DIR run lint"
]
},
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"function": "app"
}
]
}
}
これが私のpublic/auth.js
、トークンが要求され、クライアントで受信されます。これは私が行き詰まるところです:
/* global firebase, firebaseui */
const uiConfig = {
// signInSuccessUrl: '<url-to-redirect-to-on-success>',
signInOptions: [
// Leave the lines as is for the providers you want to offer your users.
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
// firebase.auth.FacebookAuthProvider.PROVIDER_ID,
// firebase.auth.TwitterAuthProvider.PROVIDER_ID,
// firebase.auth.GithubAuthProvider.PROVIDER_ID,
firebase.auth.EmailAuthProvider.PROVIDER_ID
// firebase.auth.PhoneAuthProvider.PROVIDER_ID
],
callbacks: {
signInSuccess () { return false }
}
// Terms of service url.
// tosUrl: '<your-tos-url>'
}
const ui = new firebaseui.auth.AuthUI(firebase.auth())
ui.start('#firebaseui-auth-container', uiConfig)
firebase.auth().onAuthStateChanged(function (user) {
if (user) {
firebase.auth().currentUser.getIdToken().then(token => {
console.log('You are an authorized user.')
// This is insecure. What should I do instead?
// document.cookie = '__session=' + token
})
} else {
console.warn('You are an unauthorized user.')
}
})
クライアント側で認証済みIDトークンをどうすればよいですか?
Cookies/localStorage/webStorageは、少なくとも私が見つけることができる比較的単純でスケーラブルな方法では、完全にセキュリティ保護できるようには見えません。リクエストヘッダーにトークンを直接含めるのと同じくらい安全な簡単なCookieベースのプロセスがあるかもしれませんが、Firebaseに簡単に適用できるコードを見つけることができませんでした。
私はAJAXリクエストにトークンを含める方法を知っています、例えば:
var xhr = new XMLHttpRequest()
xhr.open('GET', URL)
xmlhttp.setRequestHeader("Authorization", 'Bearer ' + token)
xhr.onload = function () {
if (xhr.status === 200) {
alert('Success: ' + xhr.responseText)
}
else {
alert('Request failed. Returned status of ' + xhr.status)
}
}
xhr.send()
ただし、1ページのアプリケーションを作成したくないので、AJAXを使用できません。有効なhref
が設定されたアンカータグをクリックしてトリガーされるリクエストなど、通常のルーティングリクエストのヘッダーにトークンを挿入する方法を理解できません。これらのリクエストを傍受して、どうにかして変更する必要がありますか?
単一ページアプリケーションではないFirebase for Webアプリケーションでのスケーラブルなクライアント側セキュリティのベストプラクティスは何ですか?複雑な認証フローは必要ありません。信頼して簡単に実装できるセキュリティシステムの柔軟性を犠牲にしてもかまいません。
Cookieが保護されないのはなぜですか?
document.cookie = "role=admin"
。 (出来上がり!)あなたは心配する必要がありますか?
私たちは何をすべき?
単一ページアプリケーションを使用している場合は、トークンをどこにも保存せず、それをJS変数に保持して、Authorizationヘッダーを使用してajaxリクエストを作成します。 jQueryを使用している場合は、beforeSend
ハンドラーをグローバルに追加できます ajaxSetup
ajaxリクエストを行うたびにAuthトークンヘッダーを送信します。
var token = false; /* you will set it when authorized */
$.ajaxSetup({
beforeSend: function(xhr) {
/* check if token is set or retrieve it */
if(token){
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
}
}
});
クッキーを使いたい場合
シングルページアプリケーションを実装せずにCookieを使いたくない場合は、2つのオプションから選択できます。
document.cookie = '__session=' + token /* Non-Persistent */
document.cookie = '__session=' + token + ';max-age=' + (3600*24*7) /* Persistent 1 week */
どちらを使用するか、永続的か非永続的か、選択は完全にプロジェクトに依存します。また、永続的なCookieの場合、max-ageは1か月または1時間ではなく、バランスをとる必要があります。 1週間か2週間は私にとってより良い選択肢に見えます。
Firebase IDトークンをCookieに保存することにあまり懐疑的です。 Cookieに保存することにより、Firebase Cloud関数へのすべてのリクエストで送信されます。
Firebase IDトークン:
ユーザーがFirebaseアプリにログインしたときにFirebaseによって作成されます。これらのトークンは、Firebaseプロジェクトでユーザーを安全に識別する署名付きのJWTです。これらのトークンには、Firebaseプロジェクトに固有のユーザーのID文字列など、ユーザーの基本的なプロファイル情報が含まれています。 IDトークンの整合性を検証できるので、IDトークンをバックエンドサーバーに送信して、現在サインインしているユーザーを識別できます。
Firebase IDトークンの定義で述べたように、トークンの整合性を検証できるため、サーバーに保存して送信しても安全です。ルーティングにAJAXリクエストを使用しないようにするため、Firebase Cloud Functionへのすべてのリクエストの認証ヘッダーでこのトークンを提供する必要がないようにしたくないという問題が発生します。
これにより、Cookieはサーバー要求とともに自動的に送信されるため、Cookieの利用に戻ります。それらは、あなたが思っているほど危険ではありません。 Firebaseには、Firebase IDトークンを送信するためにセッションCookieを利用する「 ハンドルバーテンプレートおよびユーザーセッションを含むサーバー側で生成されたページ 」というサンプルアプリケーションさえあります。
この例を見ることができます here :
// Express middleware that checks if a Firebase ID Tokens is passed in the `Authorization` HTTP
// header or the `__session` cookie and decodes it.
// The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header like this:
// `Authorization: Bearer <Firebase ID Token>`.
// When decoded successfully, the ID Token content will be added as `req.user`.
const validateFirebaseIdToken = (req, res, next) => {
console.log('Check if request is authorized with Firebase ID token');
return getIdTokenFromRequest(req, res).then(idToken => {
if (idToken) {
return addDecodedIdTokenToRequest(idToken, req);
}
return next();
}).then(() => {
return next();
});
};
/**
* Returns a Promise with the Firebase ID Token if found in the Authorization or the __session cookie.
*/
function getIdTokenFromRequest(req, res) {
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
console.log('Found "Authorization" header');
// Read the ID Token from the Authorization header.
return Promise.resolve(req.headers.authorization.split('Bearer ')[1]);
}
return new Promise((resolve, reject) => {
cookieParser(req, res, () => {
if (req.cookies && req.cookies.__session) {
console.log('Found "__session" cookie');
// Read the ID Token from cookie.
resolve(req.cookies.__session);
} else {
resolve();
}
});
});
}
これにより、AJAXが不要になり、ルートがFirebase Cloud Functionで処理されるようになります。すべてのヘッダーをチェックしているFirebaseのテンプレートを確認してください- ページ 。
<script>
function checkCookie() {
// Checks if it's likely that there is a signed-in Firebase user and the session cookie expired.
// In that case we'll hide the body of the page until it will be reloaded after the cookie has been set.
var hasSessionCookie = document.cookie.indexOf('__session=') !== -1;
var isProbablySignedInFirebase = typeof Object.keys(localStorage).find(function (key) {
return key.startsWith('firebase:authUser')
}) !== 'undefined';
if (!hasSessionCookie && isProbablySignedInFirebase) {
var style = document.createElement('style');
style.id = '__bodyHider';
style.appendChild(document.createTextNode('body{display: none}'));
document.head.appendChild(style);
}
}
checkCookie();
document.addEventListener('DOMContentLoaded', function() {
// Make sure the Firebase ID Token is always passed as a cookie.
firebase.auth().addAuthTokenListener(function (idToken) {
var hadSessionCookie = document.cookie.indexOf('__session=') !== -1;
document.cookie = '__session=' + idToken + ';max-age=' + (idToken ? 3600 : 0);
// If there is a change in the auth state compared to what's in the session cookie we'll reload after setting the cookie.
if ((!hadSessionCookie && idToken) || (hadSessionCookie && !idToken)) {
window.location.reload(true);
} else {
// In the rare case where there was a user but it could not be signed in (for instance the account has been deleted).
// We un-hide the page body.
var style = document.getElementById('__bodyHider');
if (style) {
document.head.removeChild(style);
}
}
});
});
</script>
安全なトークンライブラリの生成 を使用し、トークンを直接追加します( カスタム認証ペイロード ):
var token = tokenGenerator.createToken({ "uid": "1234", "isModerator": true });
トークンデータはuid
(またはapp_user_id)およびisModerator
がルールの式内にあります。次に例を示します。
{
"rules": {
".read": true,
"$comment": {
".write": "(!data.exists() && newData.child('user_id').val() == auth.uid) || auth.isModerator == true"
}
}
}