如何保护firebase云功能HTTP端点只允许firebase认证用户?

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

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

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

我该怎么解呢?

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

request.get('x-myheader')

但是有没有一种方法可以像保护实时数据库一样保护端点呢?

98874 次浏览

对于你要做的事情,有一个官方的代码示例。它演示了如何设置HTTPS函数,以要求使用客户端在身份验证期间接收到的令牌作为授权标头。该函数使用firebase-admin库来验证令牌。

此外,如果你的应用程序能够使用Firebase客户端库,你可以使用"可调用的函数"来简化这个样板文件。

正如@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来验证该令牌。您将在响应中获得该用户的解码信息。否则,如果令牌无效,它将抛出一个错误。

正如@Doug提到的, 你可以使用可调用的函数来从你的客户端和服务器端排除一些样板代码

示例可调用函数:

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


/** This scope is reachable for authenticated users only */


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

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

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

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

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

你可以将一个函数设置为“私有”,这样它不能将被除了注册用户(你决定权限)之外的其他用户调用。在这种情况下,未经身份验证的请求将在函数的上下文中被拒绝,函数将被调用

这里是对(a) 将功能配置为public/private和(b) 向最终用户验证您的函数的引用。

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

这里有一个使用Express的很好的官方示例-将来可能会很方便:https://github.com/firebase/functions-samples/blob/master/authorized-https-endpoint/functions/index.js(粘贴在下面)

请记住,exports.app使你的函数在/app段下可用(在这种情况下,只有一个函数在<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不同,并且身份识别代理不直接支持)轻松地对用户进行身份验证。

下面是一个使用实用程序的示例:

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用于云功能 - jluddrzej

在Firebase中,为了简化你的代码和工作,只需要建筑设计:

  1. 供公众查阅的网站/内容,使用HTTPS由Express触发。若要仅限制同一站点或只适用于特定网站,请使用CORS来控制这方面的安全性。这是有意义的,因为Express对SEO很有用,因为它的服务器端渲染内容。
  2. 对于需要用户身份验证的应用程序,使用HTTPS可调用的Firebase函数,然后使用context参数来节省所有的麻烦。这也是有道理的,因为比如用AngularJS构建的单页应用——AngularJS不利于SEO,但由于它是一个密码保护的应用,你也不需要太多的SEO。至于模板,AngularJS有内置模板,所以不需要使用Express的服务器端模板。然后Firebase可调用函数应该足够好。

有了以上的思想,没有更多的麻烦,让生活更容易。

你可以把它当作一个函数返回布尔值。如果用户验证与否,您将继续或停止您的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;
}
}

这里有很多很棒的信息真的对我很有帮助,但是我认为,对于那些第一次使用Angular尝试使用它的人来说,分解一个简单的工作示例可能会很好。谷歌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
})