如何保护 firebase Cloud Function HTTP 端点只允许经过 Firebase 身份验证的用户?
How to protect firebase Cloud Function HTTP endpoint to allow only Firebase authenticated users?
-
firebase
-
firebase-security
-
firebase-authentication
-
firebase-realtime-database
-
google-cloud-functions
有了新的 firebase 云功能,我决定将我的一些 HTTP 端点移动到 firebase。
一切都很好......但我有以下问题。我有两个由 HTTP 触发器(云函数)构建的端点
- 用于创建用户的 API 端点和 returns 自定义令牌
由 Firebase Admin SDK 生成。
- 一个 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 中,为了简化您的代码和您的工作,这只是架构设计的问题:
- 对于 public 可访问 sites/contents,请使用 HTTPS triggers with
Express
。要仅限制同一站点或仅特定站点,请使用CORS
来控制这方面的安全性。这是有道理的,因为 Express
由于其服务器端呈现内容而对 SEO 很有用。
- 对于需要用户认证的应用程序,使用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
})
firebase
firebase-security
firebase-authentication
firebase-realtime-database
google-cloud-functions
有了新的 firebase 云功能,我决定将我的一些 HTTP 端点移动到 firebase。 一切都很好......但我有以下问题。我有两个由 HTTP 触发器(云函数)构建的端点
- 用于创建用户的 API 端点和 returns 自定义令牌 由 Firebase Admin SDK 生成。
- 一个 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 中,为了简化您的代码和您的工作,这只是架构设计的问题:
- 对于 public 可访问 sites/contents,请使用 HTTPS triggers with
Express
。要仅限制同一站点或仅特定站点,请使用CORS
来控制这方面的安全性。这是有道理的,因为Express
由于其服务器端呈现内容而对 SEO 很有用。 - 对于需要用户认证的应用程序,使用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
})