首页 > 解决方案 > 谷歌与 Meteor 的一键式集成

问题描述

我正在将Meteor应用程序与 Google 的One Tap集成。尝试使用 MeteorloginWithGoogle来让用户保存到Meteor 帐户(内置在 Meteor.js 中)。其复杂性在于

One-Tap 库不是为了授权用户(即产生访问令牌),只是为了验证用户

因此,我必须做的是使用 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'];
}

尝试在客户端/服务器上以不同的方式编写此代码,以及考虑解决此问题的方法并仅注册 Meteor 的Accounts.createUser,但这并不理想。[options]我传递给的有什么问题loginWithGoogle?我会认为accessToken并且idToken足够了......

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

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

谷歌一键失败2

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

第二个请求谷歌一键...

文档说明loginWithGoogle格式通常为:

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

关于loginWithGoogle

选项还可能包括Google 的附加 URI 参数


Google 的附加 URI 参数

必需:client_id、nonce、response_type、redirect_uri、范围

可选:access_type、display、hd、include_granted_scopes、login_hint、prompt


不幸的是,它显然没有识别出[options]我正在传递的东西,否则它会将用户保存到 MongoDB,而它没有这样做。

标签: node.jsgoogle-signingoogle-authenticationmeteor-accountsgoogle-one-tap

解决方案


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

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

客户

// 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;
}

推荐阅读