【问题标题】:How to protect firebase Cloud Function HTTP endpoint to allow only Firebase authenticated users?如何保护 Firebase Cloud Function HTTP 端点以仅允许经过 Firebase 身份验证的用户?
【发布时间】:2017-08-02 17:28:05
【问题描述】:

借助新的 firebase 云功能,我决定将我的一些 HTTP 端点移动到 firebase。 一切都很好......但我有以下问题。我有两个由 HTTP 触发器(云函数)构建的端点

  1. 用于创建用户并返回自定义令牌的 API 端点 由 Firebase Admin SDK 生成。
  2. 用于获取特定用户详细信息的 API 端点。

虽然第一个端点很好,但对于我的第二个端点,我只想为经过身份验证的用户保护它。意思是拥有我之前生成的令牌的人。

我该如何解决这个问题?

我知道我们可以使用云函数中的 Header 参数

request.get('x-myheader')

但是有没有办法像保护实时数据库一样保护端点?

【问题讨论】:

标签: firebase firebase-realtime-database firebase-authentication firebase-security google-cloud-functions


【解决方案1】:

有一个官方的code sample 来表示您正在尝试做的事情。它说明了如何设置您的 HTTPS 函数以要求 Authorization 标头带有客户端在身份验证期间收到的令牌。该函数使用 firebase-admin 库来验证令牌。

此外,如果您的应用能够使用 Firebase 客户端库,您可以使用“callable functions”来简化这些样板文件。

【讨论】:

  • 此代码示例仍然有效吗?这仍然是您今天要解决的问题吗?
  • @GalBracha 今天应该仍然有效(2017 年 10 月 31 日)。
  • 使用可调用函数如何使样板文件更容易?据我了解,这些只是“非 REST”服务器功能,我不太了解它们在这里的关系。谢谢。
  • @1252748 如果您阅读链接的文档,就会清楚。它会自动处理身份验证令牌的传递和验证,因此您不必在任何一方编写该代码。
  • 我觉得这个例子很糟糕。为什么要在函数中创建快速应用程序?
【解决方案2】:

正如@Doug 所述,您可以使用firebase-admin 来验证令牌。我已经建立了一个简单的例子:

exports.auth = functions.https.onRequest((req, res) => {
  cors(req, res, () => {
    const tokenId = req.get('Authorization').split('Bearer ')[1];
    
    return admin.auth().verifyIdToken(tokenId)
      .then((decoded) => res.status(200).send(decoded))
      .catch((err) => res.status(401).send(err));
  });
});

在上面的示例中,我还启用了 CORS,但这是可选的。首先,获取Authorization 标头并找出token

然后,您可以使用firebase-admin 来验证该令牌。您将在响应中获得该用户的解码信息。否则,如果令牌无效,则会抛出错误。

【讨论】:

  • 赞成,因为它很简单,并且不像官方示例那样依赖 express。
  • 你能详细解释一下cors吗?
  • @pete: cors 只是解决跨域资源共享。您可以谷歌了解更多信息。
  • @pete Cors 允许您从不同的 url 访问该 firebase-backend 端点。
  • @RezaRahmati 您可以在客户端使用getIdToken() 方法(例如firebase.auth().currentUser.getIdToken().then(token => console.log(token))firebase docs
【解决方案3】:

正如@Doug 也提到的, 您可以使用Callable Functions 以便从您的客户端和服务器发送exclude some boilerplate code

可调用函数示例:

export const getData = functions.https.onCall((data, context) => {
  // verify Firebase Auth ID token
  if (!context.auth) {
    return { message: 'Authentication Required!', code: 401 };
  }

  // do your things..
  const uid = context.auth.uid;
  const query = data.query;

  return { message: 'Some Data', code: 400 };
});

它可以像这样直接从您的客户端调用:

firebase.functions().httpsCallable('getData')({query}).then(result => console.log(result));

【讨论】:

    【解决方案4】:

    上述方法使用函数内部的逻辑对用户进行身份验证,因此仍必须调用该函数来进行检查。

    这是一个非常好的方法,但为了全面起见,还有另一种方法:

    您可以将一个函数设置为“私有”,以便它不能被注册用户调用(由您决定权限)。在这种情况下,未经身份验证的请求在函数上下文之外被拒绝,并且函数根本被调用。

    这里引用了 (a) Configuring functions as public/private,然后是 (b) authenticating end-users to your functions

    请注意,上面的文档是针对 Google Cloud Platform 的,实际上,这是有效的,因为每个 Firebase 项目也是一个 GCP 项目。与此方法相关的一个警告是,在撰写本文时,它仅适用于基于 Google 帐户的身份验证。

    【讨论】:

    • 不幸的是,Firebase 似乎仍然不支持 Firebase 用户的此 OOTB - 如果有人通过了身份验证令牌,那很好;但如果没有,该函数仍然会被调用:(
    • 所以基本上如果我们通过删除allUsers 角色来删除Allow unauthenticated,我们就不能使用可调用函数(onCall)?我不认为将函数公开是理想的,必须有一种方法可以将函数设置为可从特定服务帐户调用。 @JanakaBandara
    【解决方案5】:

    这里有很多非常有用的信息对我很有帮助,但我认为为第一次尝试使用 Angular 的任何人分解一个简单的工作示例可能会很好。可以在 https://firebase.google.com/docs/auth/admin/verify-id-tokens#web 找到 Google Firebase 文档。

    //#### YOUR TS COMPONENT FILE #####
    import { Component, OnInit} from '@angular/core';
    import * as firebase from 'firebase/app';
    import { YourService } from '../services/yourservice.service';
    
    @Component({
      selector: 'app-example',
      templateUrl: './app-example.html',
      styleUrls: ['./app-example.scss']
    })
    
    export class AuthTokenExample implements OnInit {
    
    //property
    idToken: string;
    
    //Add your service
    constructor(private service: YourService) {}
    
    ngOnInit() {
    
        //get the user token from firebase auth
        firebase.auth().currentUser.getIdToken(true).then((idTokenData) => {
            //assign the token to the property
            this.idToken = idTokenData;
            //call your http service upon ASYNC return of the token
            this.service.myHttpPost(data, this.idToken).subscribe(returningdata => {
                console.log(returningdata)
            });
    
        }).catch((error) => {
            // Handle error
            console.log(error);
        });
    
      }
    
    }
    
    //#### YOUR SERVICE #####
    //import of http service
    import { Injectable } from '@angular/core';
    import { HttpClient, HttpHeaders } from '@angular/common/http';
    import { Observable } from 'rxjs';
    
    @Injectable({
      providedIn: 'root'
    })
    
    export class MyServiceClass {
    
        constructor(private http: HttpClient) { }
    
      //your myHttpPost method your calling from your ts file
      myHttpPost(data: object, token: string): Observable<any> {
    
        //defining your header - token is added to Authorization Bearer key with space between Bearer, so it can be split in your Google Cloud Function
        let httpOptions = {
            headers: new HttpHeaders({
                'Content-Type': 'application/json',
             'Authorization': 'Bearer ' + token
            })
        }
    
        //define your Google Cloud Function end point your get from creating your GCF
        const endPoint = ' https://us-central1-your-app.cloudfunctions.net/doSomethingCool';
    
        return this.http.post<string>(endPoint, data, httpOptions);
    
      }
    
    }
    
    
    //#### YOUR GOOGLE CLOUD FUNCTION 'GCF' #####
    //your imports
    const functions = require('firebase-functions');
    const admin = require('firebase-admin');
    const cors = require('cors')({origin: true});
    
    
    exports.doSomethingCool = functions.https.onRequest((req, res) => {
    
    //cross origin middleware
        cors(req, res, () => {
    
            //get the token from the service header by splitting the Bearer in the Authorization header 
            const tokenId = req.get('Authorization').split('Bearer ')[1];
    
            //verify the authenticity of token of the user
            admin.auth().verifyIdToken(tokenId)
                .then((decodedToken) => {
                    //get the user uid if you need it.
                   const uid = decodedToken.uid;
    
                    //do your cool stuff that requires authentication of the user here.
    
                //end of authorization
                })
                .catch((error) => {
                    console.log(error);
                });
    
        //end of cors
        })
    
    //end of function
    })
    

    【讨论】:

      【解决方案6】:

      有一个使用 Express 的不错的官方示例 - 将来可能会派上用场:https://github.com/firebase/functions-samples/blob/master/authorized-https-endpoint/functions/index.js(肯定会粘贴在下面)

      请记住,exports.app 使您的函数在 /app slug 下可用(在这种情况下,只有一个函数在 &lt;you-firebase-app&gt;/app/hello 下可用。要摆脱它,您实际上需要稍微重写 Express 部分(用于验证的中间件部分保持不变 - 它工作得非常好,并且由于 cmets 很容易理解)。

      /**
       * Copyright 2016 Google Inc. All Rights Reserved.
       *
       * Licensed under the Apache License, Version 2.0 (the "License");
       * you may not use this file except in compliance with the License.
       * You may obtain a copy of the License at
       *
       *      http://www.apache.org/licenses/LICENSE-2.0
       *
       * Unless required by applicable law or agreed to in writing, software
       * distributed under the License is distributed on an "AS IS" BASIS,
       * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
       * See the License for the specific language governing permissions and
       * limitations under the License.
       */
      'use strict';
      
      const functions = require('firebase-functions');
      const admin = require('firebase-admin');
      admin.initializeApp();
      const express = require('express');
      const cookieParser = require('cookie-parser')();
      const cors = require('cors')({origin: true});
      const app = express();
      
      // Express middleware that validates Firebase ID Tokens passed in the Authorization HTTP header.
      // 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 = async (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 && 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 if(req.cookies) {
          console.log('Found "__session" cookie');
          // Read the ID Token from cookie.
          idToken = req.cookies.__session;
        } else {
          // No cookie
          res.status(403).send('Unauthorized');
          return;
        }
      
        try {
          const decodedIdToken = await admin.auth().verifyIdToken(idToken);
          console.log('ID Token correctly decoded', decodedIdToken);
          req.user = decodedIdToken;
          next();
          return;
        } catch (error) {
          console.error('Error while verifying Firebase ID token:', error);
          res.status(403).send('Unauthorized');
          return;
        }
      };
      
      app.use(cors);
      app.use(cookieParser);
      app.use(validateFirebaseIdToken);
      app.get('/hello', (req, res) => {
        res.send(`Hello ${req.user.name}`);
      });
      
      // This HTTPS endpoint can only be accessed by your Firebase Users.
      // Requests need to be authorized by providing an `Authorization` HTTP header
      // with value `Bearer <Firebase ID Token>`.
      exports.app = functions.https.onRequest(app);
      

      我重写以摆脱/app

      const hello = functions.https.onRequest((request, response) => {
        res.send(`Hello ${req.user.name}`);
      })
      
      module.exports = {
        hello
      }
      

      【讨论】:

        【解决方案7】:

        我一直在努力在 golang GCP 功能中获得正确的 firebase 身份验证。实际上没有例子,所以我决定建立这个小库:https://github.com/Jblew/go-firebase-auth-in-gcp-functions

        现在您可以使用 firebase-auth 轻松验证用户身份(这与 gcp-authenticated-functions 不同,并且不直接由 identity-aware-proxy 支持)。

        以下是使用该实用程序的示例:

        import (
          firebaseGcpAuth "github.com/Jblew/go-firebase-auth-in-gcp-functions"
          auth "firebase.google.com/go/auth"
        )
        
        func SomeGCPHttpCloudFunction(w http.ResponseWriter, req *http.Request) error {
           // You need to provide 1. Context, 2. request, 3. firebase auth client
          var client *auth.Client
            firebaseUser, err := firebaseGcpAuth.AuthenticateFirebaseUser(context.Background(), req, authClient)
            if err != nil {
            return err // Error if not authenticated or bearer token invalid
          }
        
          // Returned value: *auth.UserRecord
        }
        

        请记住使用 --allow-unauthenticated 标志部署您的函数(因为 firebase 身份验证发生在函数执行内部)。

        希望这对你有帮助,就像它帮助了我一样。出于性能原因,我决定将 golang 用于云功能 — Jędrzej

        【讨论】:

          【解决方案8】:

          在 Firebase 中,为了简化您的代码和工作,只需要架构设计

          1. 对于可公开访问的网站/内容,请使用HTTPS triggers with Express。要仅限制同一站点或仅限特定站点,请使用 CORS 来控制这方面的安全性。这是有道理的,因为Express 因其服务器端呈现内容而对 SEO 很有用。
          2. 对于需要用户身份验证的应用,使用HTTPS Callable Firebase Functions,然后使用context 参数即可省去所有麻烦。这也是有道理的,因为例如使用 AngularJS 构建的单页应用程序 - AngularJS 对 SEO 不利,但由于它是受密码保护的应用程序,因此您也不需要太多 SEO。至于模板,AngularJS 已经内置了模板,所以不需要Express 的服务器端模板。那么 Firebase 可调用函数应该足够好了。

          牢记以上几点,不再麻烦,让生活更轻松。

          【讨论】:

            【解决方案9】:

            您可以将此作为函数返回布尔值。如果用户验证与否,那么您将继续或停止您的 API。此外,您可以从变量 decode 返回声明或用户结果

            const authenticateIdToken = async (
                req: functions.https.Request,
                res: functions.Response<any>
            ) => {
                try {
                    const authorization = req.get('Authorization');
                    if (!authorization) {
                        res.status(400).send('Not Authorized User');
                        return false;
                    }
                    const tokenId = authorization.split('Bearer ')[1];
            
                    return await auth().verifyIdToken(tokenId)
                        .then((decoded) => {
                            return true;
                        })
                        .catch((err) => {
                            res.status(401).send('Not Authorized User')
                            return false;
                        });
                } catch (e) {
                    res.status(400).send('Not Authorized User')
                    return false;
                }
            }
            

            【讨论】:

              猜你喜欢
              • 2021-07-17
              • 2018-07-12
              • 2019-09-07
              • 2021-01-06
              • 1970-01-01
              • 2019-11-18
              • 2022-01-22
              • 2020-03-06
              • 2021-11-22
              相关资源
              最近更新 更多