node.js - NodeJS + Google Login + Firebase Functions 导致解码 Firebase 会话 cookie 失败
问题描述
我需要一个 Firebase 应用程序的 Google 登录,为了完成这项工作,我使用了多个来源:
- auth-sessions通过 NodeJS + Firebase + Google Login 提供了一个很好的开箱即用示例。
- 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
代码成功创建会话后,会发生此错误:
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!');
});
日志以 a 响应,Token verified
并将状态 200 发送到客户端。
然后,客户端重定向到经过身份验证的 URL ,该 URL/user/dashboard
执行身份验证检查(见下文),但失败并重定向回/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');
});
};
这是 express 应用程序的中间件:
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 应用程序Cookie:{}
1:12:02.911 PM 应用程序验证错误
1:47:41.905 PM 应用程序身份验证/参数错误
1:12:02.911 PM [app]解码 Firebase 会话 cookie 失败。确保您传递了代表会话 cookie 的整个字符串 JWT。 有关如何检索会话 cookie 的详细信息,请参阅https://firebase.google.com/docs/auth/admin/manage-cookies 。
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 app功能执行开始
4:43:23.501 PM 应用身份验证
4:43:23.501 PM 应用程序创建会话
下午 4:43:23.502 应用程序/sessionLogin Cookies: {"csrfToken":"19888568527706150","session":"eyJhbGciOiJSUzI1NiIsImtpZCI6InRCME0yQSJ9.eyJpc3MiOiJodHRwczovL3Nlc3Npb24uZmlyZWJ..."}
4:43:23.503 PM 应用令牌已验证
4:43:23.503 PM app {"name":redacted,"picture":"","iss":"","aud":"",...}
下午 4:43:23.503 应用程序===============
下午 4:43:23.503 应用程序/sessionLogin#verifyIdToken Cookies: {"csrfToken":"19888568527706150","session":"eyJhbGciOiJSUzI1NiIsImtpZCI6InRCME0yQSJ9.eyJpc3MiOiJodHRwczovL3Nlc3Npb24uZmlyZWJ..."}
下午 4:43:23.634 应用程序/sessionLogin#createSessionCookie Cookies: {"csrfToken":"19888568527706150","session":"eyJhbGciOiJSUzI1NiIsImtpZCI6InRCME0yQSJ9.eyJpc3MiOiJodHRwczovL3Nlc3N..."}
下午 4:43:23.634 应用程序Cookie:
4:43:23.634 PM 应用程序“eyJhbGciOiJSUzI1NiIsImtpZCI6InRCME0yQSJ9.eyJpc3MiOiJodHRwczovL3Nlc3Npb24uZmlyZWJhc2UuZ29vZ ...”
下午 4:43:23.634 应用程序===============
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 app功能执行开始
下午 4:43:24.153 应用身份验证
4:43:24.153 PM 应用程序尝试验证会话烹饪
下午 4:43:24.153 应用程序Cookie:{}
更新 3
完全按照如下所示重写启用的 API 和 NodeJS 访问firebase.json
:
{
"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
}
}
}
解决方案
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 ^^^
}
createSessionCookie
使用in方法后必须传递您设置的 cookie,verifySessionCookie
如下所示:
// 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")
}
}
推荐阅读
- java - findViewByID 中的变量 - Android Studio - Java
- cmake - 为什么 CMake find_package 忽略链接器标志?
- dicom - 如何从所需的 DICOM 标签中得出每次旋转的切片数?
- python - 全局关键字未反映
- reactjs - 从另一个反应组件调用反应组件内的方法
- axapta - 在 AX 2012 x++ 中创建常规日记帐时刷新固定资产的价值模型
- html - Bootstrap 4 间距
- python - 为什么我在绘制饼图时遇到问题?
- python - Python:如何将函数结果输出到数据框
- c - dev c++和在线编译器有什么区别?