Google OAuth 刷新令牌未返回有效访问令牌

Ign*_*rre 2 google-calendar-api google-api firebase google-drive-api google-oauth

我有一个 Firebase 应用程序,可以对用户进行身份验证并返回一个访问令牌,然后我可以使用该令牌来访问 Google Calendar 和 Sheets API。我还保存了刷新令牌。经过身份验证的令牌的示例代码:

firebase
  .signInWithGoogle()
  .then(async (socialAuthUser) => { 
    let accessToken = socialAuthUser.credential.accessToken // token to access Google Sheets API
    let refreshToken = socialAuthUser.user.refreshToken
    this.setState({accessToken, refreshToken}) 
 })
Run Code Online (Sandbox Code Playgroud)

1 小时后,accessToken 将过期。Firebase 身份验证在登录后在用户对象上提供刷新令牌

我使用该刷新令牌重新进行身份验证并通过发布到以下位置获取新的 access_token:

https://securetoken.googleapis.com/v1/token?key=firebaseAppAPIKey

新的访问令牌不再适用于 Google API,并且不再具有授权范围。我也尝试将其发送到

https://www.googleapis.com/oauth2/v1/tokeninfo?access_token="refreshToken"

它给我错误“无效令牌”。当我使用 firebase 的原始令牌时,它工作得很好。

还有其他人遇到类似的问题吗?我还没有找到一种方法来使用正确的访问范围刷新原始访问令牌,而不需要用户注销并再次登录。

谢谢!

Ign*_*rre 8

我经过多次尝试终于能够解决这个问题。

在 Medium 上发布了详细的解决方案:https://inaguirre.medium.com/reusing-access-tokens-in-firebase-with-react-and-node-3fde1d48cbd3

在客户端上,我将 React 与 Firebase 库结合使用,在服务器上,我将 Node.js 与链接到同一 Firebase 项目的 google-apis 包和 firebase-admin skd 包一起使用。

脚步:

  1. (CLIENT) 向服务器发送请求,生成认证链接
  2. (服务器)生成身份验证链接并使用 googleapis 中的 getAuthLink() 将其发送回客户端。使用 Google 登录并处理重定向。
  3. (服务器)在重定向路由上,在查询字符串上使用 Google 的代码来验证用户身份并获取其用户凭据。使用这些凭据检查用户是否已在 Firebase 上注册。
  4. (服务器)如​​果用户已注册,请使用 oauth2.getTokens(code) 获取访问令牌和刷新令牌,并更新数据库中用户配置文件上的刷新令牌。如果用户未注册,请使用 firebase.createUser() 创建新用户,并使用刷新令牌在数据库上创建用户配置文件。
  5. (服务器)使用 firebase.createCustomToken(userId) 将 id_token 发送回客户端并进行身份验证。
  6. (服务器)使用 res.redirect({access_token,referesh_token,id_token}) 将凭据发送回客户端。
  7. (客户端)在客户端,使用signInWithCustomToken(id_token)进行身份验证,并重构查询以获取access_token和refresh_token来发送API调用。
  8. (客户端)设置访问令牌的到期日期。对于每个请求,检查当前日期是否高于到期日期。如果是,请使用刷新令牌向https://www.googleapis.com/oauth2/v4/token请求新令牌。否则使用存储的access_token。

大多数事情发生在身份验证后处理 Google 重定向时。以下是在后端处理身份验证和令牌的示例:

const router = require("express").Router();

const { google } = require("googleapis");

const { initializeApp, cert } = require("firebase-admin/app");

const { getAuth } = require("firebase-admin/auth");
const { getDatabase } = require("firebase-admin/database");
const serviceAccount = require("../google-credentials.json");

const fetch = require("node-fetch");

initializeApp({
  credential: cert(serviceAccount),
  databaseURL: "YOUR_DB_URL",
});

const db = getDatabase();

const oauth2Client = new google.auth.OAuth2(
  process.env.GOOGLE_CLIENT_ID,
  process.env.GOOGLE_CLIENT_SECRET,
  "http://localhost:8080/handleGoogleRedirect"
);

//post to google auth api to generate auth link
router.post("/authLink", (req, res) => {
  try {
    // generate a url that asks permissions for Blogger and Google Calendar scopes
    const scopes = [
      "profile",
      "email",
      "https://www.googleapis.com/auth/drive.file",
      "https://www.googleapis.com/auth/calendar",
    ];

    const url = oauth2Client.generateAuthUrl({
      access_type: "offline",
      scope: scopes,
      // force access
      prompt: "consent",
    });
    res.json({ authLink: url });
  } catch (error) {
    res.json({ error: error.message });
  }
});

router.get("/handleGoogleRedirect", async (req, res) => {
  console.log("google.js 39 | handling redirect", req.query.code);
  // handle user login
  try {
    const { tokens } = await oauth2Client.getToken(req.query.code);
    oauth2Client.setCredentials(tokens);

    // get google user profile info
    const oauth2 = google.oauth2({
      version: "v2",
      auth: oauth2Client,
    });

    const googleUserInfo = await oauth2.userinfo.get();

    console.log("google.js 72 | credentials", tokens);

    const userRecord = await checkForUserRecord(googleUserInfo.data.email);

    if (userRecord === "auth/user-not-found") {
      const userRecord = await createNewUser(
        googleUserInfo.data,
        tokens.refresh_token
      );
      const customToken = await getAuth().createCustomToken(userRecord.uid);
      res.redirect(
        `http://localhost:3000/home?id_token=${customToken}&accessToken=${tokens.access_token}&userId=${userRecord.uid}`
      );
    } else {
      const customToken = await getAuth().createCustomToken(userRecord.uid);

      await addRefreshTokenToUserInDatabase(userRecord, tokens);

      res.redirect(
        `http://localhost:3000/home?id_token=${customToken}&accessToken=${tokens.access_token}&userId=${userRecord.uid}`
      );
    }
  } catch (error) {
    res.json({ error: error.message });
  }
});

const checkForUserRecord = async (email) => {
  try {
    const userRecord = await getAuth().getUserByEmail(email);
    console.log("google.js 35 | userRecord", userRecord.displayName);
    return userRecord;
  } catch (error) {
    return error.code;
  }
};

const createNewUser = async (googleUserInfo, refreshToken) => {
  console.log(
    "google.js 65 | creating new user",
    googleUserInfo.email,
    refreshToken
  );
  try {
    const userRecord = await getAuth().createUser({
      email: googleUserInfo.email,
      displayName: googleUserInfo.name,
      providerToLink: "google.com",
    });

    console.log("google.js 72 | user record created", userRecord.uid);

    await db.ref(`users/${userRecord.uid}`).set({
      email: googleUserInfo.email,
      displayName: googleUserInfo.name,
      provider: "google",
      refresh_token: refreshToken,
    });

    return userRecord;
  } catch (error) {
    return error.code;
  }
};

const addRefreshTokenToUserInDatabase = async (userRecord, tokens) => {
  console.log(
    "google.js 144 | adding refresh token to user in database",
    userRecord.uid,
    tokens
  );
  try {
    const addRefreshTokenToUser = await db
      .ref(`users/${userRecord.uid}`)
      .update({
        refresh_token: tokens.refresh_token,
      });
    console.log("google.js 55 | addRefreshTokenToUser", tokens);
    return addRefreshTokenToUser;
  } catch (error) {
    console.log("google.js 158 | error", error);
    return error.code;
  }
};

router.post("/getNewAccessToken", async (req, res) => {
  console.log("google.js 153 | refreshtoken", req.body.refresh_token);

  // get new access token
  try {
    const request = await fetch("https://www.googleapis.com/oauth2/v4/token", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        client_id: process.env.GOOGLE_CLIENT_ID,
        client_secret: process.env.GOOGLE_CLIENT_SECRET,
        refresh_token: req.body.refresh_token,
        grant_type: "refresh_token",
      }),
    });
    const data = await request.json();
    console.log("google.js 160 | data", data);
    res.json({
      token: data.access_token,
    });
  } catch (error) {
    console.log("google.js 155 | error", error);
    res.json({ error: error.message });
  }
});

module.exports = router;
Run Code Online (Sandbox Code Playgroud)