首页 > 解决方案 > NodeJS + Google Login + Firebase Functions 导致解码 Firebase 会话 cookie 失败

问题描述

我需要一个 Firebase 应用程序的 Google 登录,为了完成这项工作,我使用了多个来源:

问题:

尝试在生产环境中登录 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
    }
  }
}

标签: node.jsfirebase-authenticationgoogle-cloud-functionsfirebase-admin

解决方案


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")
  }
}

推荐阅读