Google 与 Meteor 的一键集成

Google One Tap Integration with Meteor

我正在集成一个 Meteor application with Google's One Tap. Attempting to use Meteor's loginWithGoogle in order to get the user to save to Meteor Accounts(内置于 Meteor.js)。其复杂性在于

One-Tap library is not meant to authorize the user (i.e. produce Access Token), only to authenticate the user

因此,我必须做的是使用 Google Api 或 gapi 对用户进行身份验证以检索必要的 access_tokenid_token.支持 .

目前我得到的信息如下:

HTML

<div data-prompt_parent_id="g_id_onload" style={{ position: "absolute", top: "5em", right: "1em" }} id="g_id_onload"></div>

客户端

google.accounts.id.initialize({
  prompt_parent_id: "g_id_onload",
  client_id: "42424242-example42.apps.googleusercontent.com",
  auto_select: false,
  callback: handleCredentialResponse
});

const handleCredentialResponse = async oneTapResponse => {
  // see the SERVER SIDE code, which is where validation of One Tap response happens
  Meteor.call("verifyOneTap", oneTapResponse.credential, oneTapResponse.clientId, (error, result) => {
    if (error) {
      console.log(error);
    }
    if (result) {
      // Initialize the JavaScript client library.
      gapi.load("auth2", function() {
        // Ready. Make a call to gapi.auth2.init or some other API 
        gapi.auth2.authorize(
          {
            client_id: oneTapResponse.clientId,
            scope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
            response_type: "code token id_token",
            prompt: "none",
            // this is the actual email address of user, example@gmail.com, passed back from the server where we validated the One Tap event...
            login_hint: result.email
          },
          function(result, error) {
            if (error) {
              // An error happened.
              console.log(error);
              return;
            }
            //these are the authentication tokens taht are so difficult to capture...
            let theAccessToken = result.access_token;
            let theIdToken = result.id_token;

            //*********************************
            //this is the part that doesn't work
            //trying to get it to create the account without another Google prompt...
            Meteor.loginWithGoogle({ accessToken: theAccessToken, idToken: theIdToken, prompt: "none" }, function(err, res) {
              if (err) {
                console.log(err)
              }
            });
            //*********************************
          }
        );
      });
    }
  });
};

google.accounts.id.prompt(notification => {
  //this just tells you when things go wrong...
  console.log(notification);
});

服务器端

const { OAuth2Client } = require("google-auth-library");
const clientOA2 = new OAuth2Client("42424242-example42.apps.googleusercontent.com");

// the token and clientId are returned from One Tap in an object, are credential (token) and clientId (clientId)
verifyOneTap: async (token, clientId) => {
  const ticket = await clientOA2.verifyIdToken({
    idToken: token,
    audience: clientId // Specify the CLIENT_ID of the app that accesses the backend
    // Or, if multiple clients access the backend:
    //[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
  });
  const payload = await ticket.getPayload();

  //perform validation here so you don't get hacked...

  return payload;
  // If request specified a G Suite domain:
  // const domain = payload['hd'];
}

尝试在 client/server 上以不同的方式写这篇文章,并考虑过解决这个问题的方法,只是注册了 Meteor 的 Accounts.createUser,但它不太理想。我传递给 loginWithGoogle[options] 有什么问题?我认为 accessTokenidToken 就足够了...

发生的事情是,在登录时,它确实通过 Google One Tap 的第一阶段让我登录,但是我投入 Meteor.loginWithGoogle 的选项不知何故无法被识别:

这有效(一步流程的第一步)=>

但随后它再次要求登录:|

loginWithGoogle 上的 documentation 声明格式通常为:

Meteor.loginWith<ExternalService>([options], [callback])

关于loginWithGoogle

options may also include Google’s additional URI parameters


Google 的附加 URI 参数

必填: client_id、随机数、response_type、redirect_uri、范围

可选: access_type、显示、高清、include_granted_scopes、login_hint、提示


不幸的是,它显然没有识别出我传递的 [options] 中的某些内容,否则它会将用户保存到 MongoDB,但它并没有这样做。

好的,找到了答案 - 我正在研究更清洁的东西,但这是当前的修复 - 谢谢 jimmy knoot and methodx 的一些启发。

注意:其他一切与上面的原始问题相同。

客户

// this is the callback from the Google One Tap `google.accounts.id.initialize` (see original Stack Overflow question above)
const handleCredentialResponse = async oneTapResponse => {
  // see the SERVER SIDE code, which is where validation of One Tap response happens
  Meteor.call("verifyOneTap", oneTapResponse.credential, oneTapResponse.clientId, (error, result) => {
    if (error) {
      console.log(error);
    }
    if (result) {
      // Initialize the JavaScript client library.
      gapi.load("auth2", function() {
        // Ready. Make a call to gapi.auth2.init or some other API 
        gapi.auth2.authorize(
          {
            client_id: oneTapResponse.clientId,
            scope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
            response_type: "code token id_token",
            prompt: "none",
            // this is the actual email address of user, example@gmail.com, passed back from the server where we validated the One Tap event...
            login_hint: result.email
          },
          function(tokens, error) {
            if (error) {
              // An error happened.
              console.log(error);
              return;
            }
            //gapi returns tokens including accessToken and idToken...
            Meteor.call("createOneTapUser", result, tokens, (error, stampedLoginToken) => {
              if (error) {
                console.log(error);
              }
              //this logs in with the token created earlier...should do whatever your normal google login functionality does...
            //*********************************
            // this is where you skip the Google login popup :) 
              Meteor.loginWithToken(stampedLoginToken);
            });
            //*********************************
          }
        );
      });
    }
  });
};

服务器

createOneTapUser: async (userDetails, accessDetails) => {
  //just including details here for what part of a user object would look like from Meteor.loginWithGoogle > note especially resume > loginTokens
  let oneTapUserObj = {
    services: {
      google: {
        accessToken: accessDetails.access_token,
        idToken: accessDetails.id_token,
        scope: ["https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", "openid"], // yours may be different...
        expiresAt: accessDetails.expires_at,
        id: userDetails.sub,
        email: userDetails.email,
        verified_email: userDetails.email_verified,
        name: userDetails.name,
        given_name: userDetails.given_name,
        family_name: userDetails.family_name,
        picture: userDetails.picture,
        locale: "en"
      },
      resume: {
        loginTokens: []
      }
    } //...whatever your user object normally looks like.
  };
  //manually inserting the user
  Meteor.users.insert(oneTapUserObj);

  let newOneTapUser = await Meteor.users.findOne({ "profile.email": userDetails.email });
  // generates the login token that goes under user > services > resume > loginTokens...
  let stampedLoginToken = Accounts._generateStampedLoginToken();
  Accounts._insertLoginToken(newOneTapUser._id, stampedLoginToken);

  //sets the social media image from the google account...you'll need to build your own...
  userDetails.picture ? scrapeSocialMediaImage(newOneTapUser._id, userDetails.picture) : console.log("Google One Tap user " + newOneTapUser._id + " has no profile picture...");

  return stampedLoginToken.token;
}