如何保护 firebase Cloud Function HTTP 端点只允许经过 Firebase 身份验证的用户?

How to protect firebase Cloud Function HTTP endpoint to allow only Firebase authenticated users?

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

  1. 用于创建用户的 API 端点和 returns 自定义令牌 由 Firebase Admin SDK 生成。
  2. 一个 API 端点,用于获取某些用户详细信息。

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

我该如何解决这个问题?

我知道我们可以使用

在云函数中获取Header参数
request.get('x-myheader')

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

有一个官方 code sample 可以满足您的要求。它说明的是如何设置您的 HTTPS 函数以要求使用客户端在身份验证期间收到的令牌进行授权 header。该函数使用 firebase-admin 库来验证令牌。

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

如@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 header 并找出 token.

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

@Doug 也提到了, 您可以从客户端和服务器使用 Callable Functions in order to

可调用函数示例:

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));

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

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

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

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

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

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

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

/**
 * 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
}

我一直在努力在 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

在 Firebase 中,为了简化您的代码和您的工作,这只是架构设计的问题:

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

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

您可以将其作为函数 returns 布尔值。如果用户验证与否,那么您将继续或停止您的 API。此外,您可以 return 声明或来自变量 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;
    }
}

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

找到
//#### 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
})