web-dev-qa-db-ja.com

Vanilla JavaScriptを使用してクライアント側でFirebase IDトークンを処理する

バニラ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アプリケーションでのスケーラブルなクライアント側セキュリティのベストプラクティスは何ですか?複雑な認証フローは必要ありません。信頼して簡単に実装できるセキュリティシステムの柔軟性を犠牲にしてもかまいません。

16

Cookieが保護されないのはなぜですか?

  1. Cookieデータは簡単に調整できます。開発者がログインしたユーザーのロールをCookieに保存するのに十分な愚かさがある場合、ユーザーはCookieデータを簡単に変更できますdocument.cookie = "role=admin"。 (出来上がり!)
  2. ハッカーはXSS攻撃によりCookieデータを簡単に取得でき、アカウントにログインできます。
  3. ブラウザからCookieデータを簡単に収集でき、ルームメイトはCookieを盗み、自分のコンピュータからログインすることができます。
  4. SSLを使用していない場合、ネットワークトラフィックを監視しているすべてのユーザーがCookieを収集できます。

あなたは心配する必要がありますか?

  1. 不正なアクセスを取得するためにユーザーが変更できるCookieには、何も馬鹿げたことはありません。
  2. ハッカーがXSS攻撃によってCookieデータを取得できる場合、シングルページアプリケーションを使用しない場合は認証トークンを取得することもできます(ローカルストレージなどのどこかにトークンを格納するため)。
  3. あなたのルームメイトは、あなたのローカルストレージデータも拾うことができます。
  4. SSLを使用しない限り、ネットワークを監視している誰もが認証ヘッダーを取得できます。 Cookieと認証はどちらも、httpヘッダーのプレーンテキストとして送信されます。

私たちは何をすべき?

  1. トークンをどこかに保存している場合、Cookieに勝るセキュリティ上の利点はありません。Authトークンは、セキュリティを追加する単一ページアプリケーション、またはCookieを使用できないオプションに最適です。
  2. 誰かがネットワークトラフィックを監視していることが懸念される場合は、SSLでサイトをホストする必要があります。 SSLが使用されている場合、Cookieおよびhttpヘッダーは傍受できません。
  3. 単一ページアプリケーションを使用している場合は、トークンをどこにも保存せず、それを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つのオプションから選択できます。

  1. 非永続的(またはセッション)Cookie:非永続的Cookieにはmax-life/expirationの日付がなく、ユーザーがブラウザウィンドウを閉じると削除されます。セキュリティが懸念される状況では、これを非常に望ましいものにします。
  2. 永続的なCookie:永続的なCookieとは、max-life/expiration dateを持つものです。これらのCookieは、期間が終了するまで存続します。ユーザーがブラウザを閉じて翌日に戻ってきた場合でもCookieが存在するようにしたい場合は、永続的なCookieを使用することをお勧めします。これにより、毎回認証が行われなくなり、ユーザーエクスペリエンスが向上します。
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週間は私にとってより良い選択肢に見えます。

4
Munim Munna

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>
2
mootrichard

安全なトークンライブラリの生成 を使用し、トークンを直接追加します( カスタム認証ペイロード ):

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"
    }
  }
}
0
5377037