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-parserbody-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,有啥区别?

Nodejs路由之间的数据传递

重定向时使用 React-Google-Login 的 Google 身份验证问题

google_maps_flutter 与 flutter_facebook_login 不兼容

使用 react-google-login 和 django-allauth 刷新令牌

WEB API AngularJS Google+Login CORS 不允许