NodeJS + Google Login + Firebase Functions 导致解码 Firebase 会话 cookie 失败
Posted
技术标签:
【中文标题】NodeJS + Google Login + Firebase Functions 导致解码 Firebase 会话 cookie 失败【英文标题】:NodeJS + Google Login + Firebase Functions results in decoding firebase session cookie failed 【发布时间】:2021-10-31 22:10:29 【问题描述】:我需要一个 Firebase 应用程序的 Google 登录,为了完成这项工作,我使用了多个来源:
auth-sessions 提供了一个很好的开箱即用的 NodeJS + Firebase + Google 登录示例。 manage firebase session 提供了一个示例 NodeJS 函数(不是完整的工作解决方案)来使用 Firebase 函数。问题:
在生产环境中尝试登录 Google 帐户时,执行以下代码验证功能时会出现以下错误:
admin
.auth()
.verifySessionCookie(sessionCookie, true /** checkRevoked */)
.then((decodedClaims) =>
log("Decode success");
// inbetween checks
log("Successfully decoded and authenticated");
next();
)
.catch((error) =>
log("Error authenticating"); < ---- THIS IS THE PROBLEM
...
);
此错误仅在生产环境(即部署到 Firebase)时发生。当使用仅模拟托管和功能的firebase模拟器进行本地测试时(auth、firestore、数据库等都是生产环境),登录成功。部署时,登录失败并出现以下错误。
错误:
解码 Firebase 会话 cookie 失败。确保您传递了代表会话 cookie 的整个字符串 JWT。有关如何检索会话 cookie 的详细信息,请参阅 https://firebase.google.com/docs/auth/admin/manage-cookies。
更多详情:
以下是执行的步骤/操作的高级概述
执行操作的步骤概述
1. Visit any page e.g. /login
2. Click sign in with Google, execute the popup provider (see [here][3])
2.
1. Sign in with Google account
2. Send token to firebase functions for verification i.e. `POST /sessionLogin`
3. Receive response (assume 200 OK)
4. Redirect to authenticated URL
错误出现在最后一步,即 4
使用在 firebase 网站上找到的示例 /sessionLogin
代码成功创建会话后会出现此错误 here:
const auth = admin.auth();
auth.verifyIdToken(idToken).then(value =>
debug("Token verified")
return auth.createSessionCookie(idToken, expiresIn)
.then((sessionCookie) =>
// Set cookie policy for session cookie.
const options = maxAge: expiresIn, httpOnly: true, secure: true;
res.cookie('session', sessionCookie, options);
// res.json(JSON.stringify(status: 'success'));
res.status(200).send("OK");
).catch((error) =>
debug(error);
res.status(401).send('UNAUTHORIZED REQUEST!');
);
).catch(reason =>
debug("Unable to verify token");
debug(reason);
res.status(401).send('INVALID TOKEN!');
);
日志以 Token verified
响应,并将状态 200 发送到客户端。
客户端然后重定向到经过身份验证的 URL /user/dashboard
,该 URL 执行身份验证检查(见下文),但失败并重定向回 /login
:
const authenticate = (req, res, next) =>
log("Authenticating");
// source: https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookie_and_check_permissions
const sessionCookie = req.cookies.session || '';
// Verify the session cookie. In this case an additional check is added to detect
// if the user's Firebase session was revoked, user deleted/disabled, etc.
return admin
.auth()
.verifySessionCookie(sessionCookie, true /** checkRevoked */)
.then((decodedClaims) =>
log("Decode success");
// inbetween checks
log("Successfully decoded and authenticated");
next();
)
.catch((error) =>
log("Error authenticating");
if(error.errorInfo && error.errorInfo.code && error.errorInfo.code === "auth/argument-error")
debug(error.errorInfo.message);
res.redirect('/user/login');
return;
debug(error);
// Session cookie is unavailable or invalid. Force user to login.
req.flash("message", [
status: false,
message: "Invalid session, please login again!"
])
res.redirect('/user/login');
);
;
这是快递应用的中间件:
admin.initializeApp(
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://my-company-default-rtdb.firebaseio.com",
storageBucket: "gs://my-company.appspot.com"
);
const app = express();
app.use(cors(origin: true));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded(extended: true));
app.use(morgan('dev'));
app.use(cookieParser('0000-0000-0000-0000-0000'))
app.set('trust proxy', 1) // trust first proxy
// Attach CSRF token on each request.
app.use(attachCsrfToken('/', 'csrfToken', (Math.random()* 100000000000000000).toString()));
app.use(session(
secret: '0000-0000-0000-0000-0000',
resave: false,
name: '__session',
store: new FirebaseStore(
database: admin.database()
),
));
app.use(flash());
app.use(authenticate);
// routes
exports.app = functions.https.onRequest(app);
执行日志:
1:12:02.796 PM 应用程序功能执行开始
1:12:02.910 PM 应用程序正在验证
1:12:02.910 PM 应用程序正在尝试验证会话烹饪
1:12:02.910 PM 应用程序 Cookie:
1:12:02.911 PM 应用程序验证错误
1:47:41.905 PM 应用程序 auth/argument-error
1:12:02.911 PM [app] 解码 Firebase 会话 cookie 失败。确保您传递了代表会话的整个字符串 JWT 曲奇饼。见https://firebase.google.com/docs/auth/admin/manage-cookies 有关如何检索会话 cookie 的详细信息。
1:12:02.937 PM [app] 函数执行耗时 141 毫秒,完成状态码:302
更新
调用后端进行身份验证:
const postIdTokenToSessionLogin = (idToken, csrfToken) =>
return axios(
url: "/user/sessionLogin",
method: "POST",
data:
idToken: idToken,
csrfToken: csrfToken,
,
).then(value =>
console.log(value);
if(value.status === 200)
window.location.assign("/user/dashboard");
).catch(reason =>
console.error(reason);
alert("Failed to login");
);
客户端调用:
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth()
.signInWithPopup(provider)
.then(async value =>
firebase.auth().currentUser.getIdToken().then(idToken =>
// const idToken = value.credential.idToken;
const csrfToken = getCookie('_csrf');
return postIdTokenToSessionLogin(idToken, csrfToken);
).catch(reason =>
console.error("Failed to get current user token");
alert(reason.message);
);
)/*.then(value =>
window.location.assign("/user/dashboard")
)*/.catch((error) =>
console.error("Failed to sign in with Google");
alert(error.message);
);
更新 2:
使用以下内容更新了客户端 axios 请求,还添加了额外的 req.cookies
日志记录
return axios(
url: "/user/sessionLogin",
method: "POST",
withCredentials: true,
data:
idToken: idToken,
csrfToken: csrfToken,
,
)
额外的日志记录:
4:43:23.493 PM 应用程序功能执行开始
4:43:23.501 PM 应用正在验证
4:43:23.501 PM 应用创建会话
4:43:23.502 PM app /sessionLogin Cookies: "csrfToken":"19888568527706150","session":"eyJhbGciOiJSUzI1NiIsImtpZCI6InRCME0yQSJ9.eyJpc3MiOiJodHRwczovL3Nlc3Npb24uZmlyZWJ..."
4:43:23.503 PM 应用令牌验证
4:43:23.503 PM 应用程序 "name":redacted,"picture":"","iss":"","aud":"",...
4:43:23.503 PM 应用 ==============
4:43:23.503 PM 应用程序 /sessionLogin#verifyIdToken Cookies: "csrfToken":"19888568527706150","session":"eyJhbGciOiJSUzI1NiIsImtpZCI6InRCME0yQSJ9.eyJpc3MiOiJodHRwczovL3Nlc3Npb24uZmly..."
4:43:23.634 PM 应用程序 /sessionLogin#createSessionCookie Cookies: "csrfToken":"19888568527706150","session":"eyJhbGciOiJSUzI1NiIsImtpZCI6InRCME0yQSJ9.eyJpc3MiOiJodHRwczovL3Nlc3N..."
4:43:23.634 PM 应用 Cookie:
4:43:23.634 PM 应用程序“eyJhbGciOiJSUzI1NiIsImtpZCI6InRCME0yQSJ9.eyJpc3MiOiJodHRwczovL3Nlc3Npb24uZmlyZWJhc2UuZ29vZ...”
4:43:23.634 PM 应用 ==============
4:43:23.643 PM 应用程序 [0mPOST /user/sessionLogin [32m200[0m 139.036 ms - 2[0m
4:43:23.643 PM app 函数执行耗时 150 毫秒,完成状态码:200
4:43:24.131 PM 应用功能执行开始
4:43:24.153 PM 应用正在验证
4:43:24.153 PM 应用正在尝试验证会话烹饪
4:43:24.153 PM 应用 Cookie:
更新 3
完全按照firebase.json
所示重写启用的 API 和 NodeJS 访问:
"database":
"rules": "database.rules.json"
,
"firestore":
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
,
"hosting":
"site": "my-company-admin-portal",
"public": "public",
"rewrites": [
"source": "/api/**",
"function": "api"
,
"source": "**",
"function": "app"
],
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
,
"storage":
"rules": "storage.rules"
,
"emulators":
"auth":
"port": 9099
,
"functions":
"port": 5001
,
"database":
"port": 9000
,
"hosting":
"port": 5000
,
"storage":
"port": 9199
,
"ui":
"enabled": true
【问题讨论】:
当函数被部署(或调用你的服务器)时,你从哪里调用函数?本地主机或实时域?我能够复制该问题,但是从 localhost 调用该函数时。 @Dharmaraj 它在本地使用模拟器正常工作,但在现场失败。 @Dharmaraj 使用它,它可以在本地正常工作firebase emulators:start --inspect-functions --only functions,hosting,firestore,database,storage
,但在现场部署时它不会
我的意思是,该功能已部署到 live 但您从哪个域(webapp)拨打电话?任何实时域或本地主机?
@Dharmaraj 我打电话给例如https://my-company-admin-portal.web.app/user/login
【参考方案1】:
sessionCookie
在问题中提供的代码中未定义。
// Authenticate middleware right now
const authenticate = (req, res, next) =>
log("Authenticating");
// No sessionCookie declared
return admin
.auth()
.verifySessionCookie(sessionCookie, true /** checkRevoked */)
// undefined passed here ^^^
您必须通过verifySessionCookie
方法中使用createSessionCookie
后设置的cookie,如下所示:
// updated authenticate middleware
const authenticate = async (req, res, next) =>
try
log("Authenticating");
// Read the value of cookie here
const sessionCookie = req.cookies.session
// Return unauthorized error if cookie is absent
if (!sessionCookie) return res.sendStatus(401)
const decodedClaims = await admin.auth().verifySessionCookie(sessionCookie, true)
// cookie verified, continue
catch (e)
console.log(e)
return res.send("An error occurred")
【讨论】:
请看authenticate
函数更新
更新:const sessionCookie = req.cookies.session || '';
我忽略了这一点,我很抱歉。这是存在的,但总是以 null 或 undefined 结束,这似乎是我的问题的根源。
@CybeX 我会 console.log(req.cookies)
并检查会话 cookie 是否存在。除此之外,您还可以分享您从前端制作的获取请求的代码吗?
@Dharmarag 这样您就可以在执行日志中找到req.cookies
。进行身份验证时,我记录了 cookie,即
。我会在每次互动时查看req.cookies
的内容并更新帖子。
@CybeX 请分享您从前端发出的 axios/fetch 请求。我想确认是否包含凭据?【参考方案2】:
我建议你检查这个post,他们有类似的问题。
总之,如果您检查quickstart-nodejs 存储库,您必须添加cookie-parser
和body-parser
。这是他们在post 中提供的示例:
const admin = require("firebase-admin");
const functions = require("firebase-functions");
const next = require("next");
const cors = require("cors");
const express = require("express");
const cookieParser = require("cookie-parser");
const bodyParser = require("body-parser");
const dev = process.env.NODE_ENV !== "production";
const app = next( dev, conf: distDir: "next" );
const handle = app.getRequestHandler();
admin.initializeApp(
credential: admin.credential.cert("Service account key"),
databaseURL: "Path to database"
);
const server = express();
server.use(cors( origin: true ));
server.use(bodyParser.json());
// Support URL-encoded bodies.
server.use(bodyParser.urlencoded(
extended: true
));
// Support cookie manipulation.
server.use(cookieParser());
// Attach CSRF token on each request.
server.use(attachCsrfToken('/', 'csrfToken', (Math.random()* 100000000000000000).toString()));
function attachCsrfToken(url, cookie, value)
return function(req, res, next)
if (req.url === url)
res.cookie(cookie, value);
next();
server.post("/login", (req, res) =>
if (req.body && req.body.idToken)
const idToken = `$req.body.idToken`;
const expiresIn = 60 * 60 * 24 * 5 * 1000;
admin.auth().createSessionCookie(idToken, expiresIn ).then((sessionCookie) =>
const options = maxAge: expiresIn, httpOnly: true, secure: true ;
res.cookie("session", sessionCookie, options);
res.end(JSON.stringify( sessionCookie ));
, error =>
res.status(401).send(error);
);
else
res.status(401).send("Token empty");
);
server.post("/profile", (req, res) =>
if (req.cookies && req.cookies.session)
const sessionCookie = `$req.cookies.session`;
admin.auth().verifySessionCookie(
sessionCookie, true /** checkRevoked */).then((decodedClaims) =>
res.end(JSON.stringify( decodedClaims ));
).catch(error =>
res.status(401).send(error);
);
else
res.status(401).send("Session empty");
);
exports.next = functions.https.onRequest((req, res) =>
if (req.method === "POST")
if (!req.path) req.url = `/$request.url`;
return server(req, res);
return app.prepare().then(() => handle(req, res));
);
【讨论】:
以上是关于NodeJS + Google Login + Firebase Functions 导致解码 Firebase 会话 cookie 失败的主要内容,如果未能解决你的问题,请参考以下文章
google plus login vs google login,有啥区别?
重定向时使用 React-Google-Login 的 Google 身份验证问题
google_maps_flutter 与 flutter_facebook_login 不兼容