GAPI 的 OAuth - 初始登录 Javascript 后避免身份验证和授权

Posted

技术标签:

【中文标题】GAPI 的 OAuth - 初始登录 Javascript 后避免身份验证和授权【英文标题】:OAuth for GAPI - Avoid Authentication and Authorization after initial sign in for Javascript 【发布时间】:2019-03-27 23:24:03 【问题描述】:

我创建了一个 chrome 扩展程序,它可以读取电子邮件、执行某些操作并使用 google 客户端 API for javascript 创建任务。 我正在使用 chrome 身份进行身份验证和授权。 扩展按预期工作。但是,它每隔一段时间就会要求签名。我想要的是在后台授权用户脚本,这样他们就不需要在初始身份验证和授权之后一遍又一遍地执行此操作。

到目前为止我做了什么:

我读到我需要一个刷新令牌来避免这种情况。但是,刷新令牌预计将在服务器端而不是客户端进行交换和存储(这不起作用,因为后台脚本正在执行这里的工作,即客户端) 使用 gapi.auth.authorize 和立即为真。这会导致有关外部可见性的错误。当我阅读其他内容时,他们建议在服务器内使用它。我不确定如何在 chrome 扩展中做到这一点。 在getAuthToken 中将interactive 设置为false,在访问令牌过期后由于身份验证问题而开始给出错误401。

以下是我用于身份验证和授权的代码,在加载 google api 的客户端 js 文件后调用函数 onGoogleLibraryLoaded。

    var signin = function (callback) 
        chrome.identity.getAuthToken(interactive: true, callback);
    ;

    function onGoogleLibraryLoaded() 
        signin(authorizationCallback);
    

    var authorizationCallback = function (data) 
        gapi.auth.setToken(access_token: data);
        gapi.client.load('tasks', 'v1')
        gapi.client.load('gmail', 'v1', function () 

            console.log("Doing my stuff after this ..")
        );
    ;

更新: 根据答案中的建议,我对代码进行了一些更改。但是,我仍然面临同样的问题。以下是更新后的代码sn -p

jQuery.loadScript = function (url, callback) 
    jQuery.ajax(
        url: url,
        dataType: 'script',
        success: callback,
        async: false

   );

//This is the first thing that happens. i.e. loading the gapi client 
if (typeof someObject == 'undefined') $.loadScript('https://apis.google.com/js/client.js', 
    function()
    console.log("gapi script loaded...")
);

//Every 20 seconds this function runs with internally loads the tasks and gmail 
// Once the gmail module is loaded it calls the function getLatestHistoryId()
setInterval(function() 
    gapi.client.load('tasks', 'v1')
    gapi.client.load('gmail', 'v1', function()
        getLatestHistoryId()
    )
    // your code goes here...
, 20 * 1000); // 60 * 1000 milsec

// This is the function that will get user's profile and when the response is received 
// it'll check for the error i.e. error 401 through method checkForError
function getLatestHistoryId()
  prevEmailData = []

  var request = gapi.client.gmail.users.getProfile(
        'userId': 'me'
    );
    request.execute(function(response)
      console.log("User profile response...")
      console.log(response)
      if(checkForError(response))
        return
      
    )


// Now here I check for the 401 error. If there's a 401 error 
// It will call the signin method to get the token again. 
// Before calling signin it'll remove the saved token from cache through removeCachedAuthToken
// I have also tried doing it without the removeCachedAuthToken part. However the results were the same.  
// I have left console statements which are self-explanatory
function checkForError(response)
  if("code" in response && (response["code"] == 401))
    console.log(" 401 found will do the authentication again ...")
    oooAccessToken = localStorage.getItem("oooAccessTokenTG")
    console.log("access token ...")
    console.log(oooAccessToken)
    alert("401 Found Going to sign in ...")

    if(oooAccessToken)
        chrome.identity.removeCachedAuthToken(token: oooAccessToken, function()
        console.log("Removed access token")
        signin()
      )  
    
    else
      console.log("No access token found to be remove ...")
      signin()
    
    return true
  
  else
    console.log("Returning false from check error")
    return false
  


// So finally when there is 401 it returns back here and calls 
// getAuthToken with interactive true 
// What happens here is that everytime this function is called 
// there is a signin popup i.e. the one that asks you to select the account and allow permissions
// That's what is bothering me. 
// I have also created a test chrome extension and uploaded it to chrome web store. 
// I'll share the link for it separately. 

var signin = function (callback) 
    console.log(" In sign in ...")
    chrome.identity.getAuthToken(interactive: true, function(data)
        console.log("getting access token without interactive ...")
        console.log(data)

        gapi.auth.setToken(access_token: data);
        localStorage.setItem("oooAccessTokenTG", data)

        getLatestHistoryId()
    )
;

清单是这样的:


  "manifest_version": 2,

  "name": "Sign in Test Extension ",
  "description": "",
  "version": "0.0.0.8",
  "icons": 
      "16": "icon16.png", 
      "48": "icon48.png", 
      "128": "icon128.png" 
  ,
  "content_security_policy": "script-src 'self' 'unsafe-eval' https://apis.google.com; object-src 'self'",
  "browser_action": 
   "default_icon": "icon.png",
   "default_popup": "popup.html"
  ,
  "permissions": [   
    "identity",
    "storage"
   ],

  "oauth2": 
        "client_id": "1234.apps.googleusercontent.com",
        "scopes": [
            "https://www.googleapis.com/auth/gmail.readonly"
        ]
    ,
    "background":
      "scripts" : ["dependencies/jquery.min.js", "background.js"]
    

还有其他人面临同样的问题吗?

【问题讨论】:

【参考方案1】:

所以这就是我认为我的问题的答案。

一些重要的事情要知道

Chrome 登录与 gmail 登录不同。您可以让 UserA 登录到 chrome,而您计划将 chrome 扩展程序与 UserB 一起使用。 chrome.identity.getAuthToken 在这种情况下不起作用,因为它会寻找登录到 chrome 的用户。 要使用其他 google 帐户,即未登录 chrome 的帐户,您需要使用 chrome.identity.launchWebAuthFlow。以下是您可以使用的步骤。我指的是这里给出的例子 (Is it possible to get an Id token with Chrome App Indentity Api?)
    转到 google 控制台,创建您自己的项目 > 凭据 > 创建凭据 > OAuthClientID > Web 应用程序。在该页面的授权重定向 URI 字段中,输入格式为 https://.chromiumapp.org 的重定向 url。如果您不知道 chrome 扩展 ID 是什么,请参阅此 (Chrome extension id - how to find it) 这将生成一个客户端 ID,该 ID 将进入您的清单文件。忘记您之前可能创建的任何客户端 ID。假设在我们的示例中,客户端 ID 是 9999.apps.googleusercontent.com

清单文件:

    
      "manifest_version": 2,
      "name": "Test gmail extension 1",
      "description": "description",
      "version": "0.0.0.1",
      "content_security_policy": "script-src 'self' 'unsafe-eval' https://apis.google.com; object-src 'self'",
      "background": 
        "scripts": ["dependencies/jquery.min.js", "background.js"]
      ,
      "browser_action": 
       "default_icon": "icon.png",
       "default_popup": "popup.html"
      ,
      "permissions": [
        "identity",
        "storage"

      ],
      "oauth2": 
        "client_id": "9999.apps.googleusercontent.com",
        "scopes": [
          "https://www.googleapis.com/auth/gmail.readonly",
           "https://www.googleapis.com/auth/tasks"
        ]
      
    

在 background.js 中获取用户信息的示例代码

    jQuery.loadScript = function (url, callback) 
        jQuery.ajax(
            url: url,
            dataType: 'script',
            success: callback,
            async: false
       );
    
    // This is the first thing that happens. i.e. loading the gapi client 
    if (typeof someObject == 'undefined') $.loadScript('https://apis.google.com/js/client.js', 
        function()
        console.log("gapi script loaded...")
    );


    // Every xx seconds this function runs with internally loads the tasks and gmail 
    // Once the gmail module is loaded it calls the function getLatestHistoryId()
    setInterval(function() 

        gapi.client.load('tasks', 'v1')
        gapi.client.load('gmail', 'v1', function()
            getLatestHistoryId()
        )
        // your code goes here...
    , 10 * 1000); // xx * 1000 milsec

    // This is the function that will get user's profile and when the response is received 
    // it'll check for the error i.e. error 401 through method checkForError
    // If there is no error i.e. the response is received successfully 
    // It'll save the user's email address in localstorage, which would later be used as a hint
    function getLatestHistoryId()
      var request = gapi.client.gmail.users.getProfile(
            'userId': 'me'
        );
        request.execute(function(response)
          console.log("User profile response...")
          console.log(response)
          if(checkForError(response))
            return
          
            userEmail = response["emailAddress"]
            localStorage.setItem("oooEmailAddress", userEmail);
        )
    

    // Now here check for the 401 error. If there's a 401 error 
    // It will call the signin method to get the token again. 
    // Before calling the signinWebFlow it will check if there is any email address 
    // stored in the localstorage. If yes, it would be used as a login hint.  
    // This would avoid creation of sign in popup in case if you use multiple gmail accounts i.e. login hint tells oauth which account's token are you exactly looking for
    // The interaction popup would only come the first time the user uses your chrome app/extension
    // I have left console statements which are self-explanatory
    // Refer the documentation on https://developers.google.com/identity/protocols/OAuth2UserAgent >
    // Obtaining OAuth 2.0 access tokens > OAUTH 2.0 ENDPOINTS for details regarding the param options
    function checkForError(response)
      if("code" in response && (response["code"] == 401))
        console.log(" 401 found will do the authentication again ...")
        // Reading the data from the manifest file ...
        var manifest = chrome.runtime.getManifest();

        var clientId = encodeURIComponent(manifest.oauth2.client_id);
        var scopes = encodeURIComponent(manifest.oauth2.scopes.join(' '));
        var redirectUri = encodeURIComponent('https://' + chrome.runtime.id + '.chromiumapp.org');
        // response_type should be token for access token
        var url = 'https://accounts.google.com/o/oauth2/v2/auth' + 
                '?client_id=' + clientId + 
                '&response_type=token' + 
                '&redirect_uri=' + redirectUri + 
                '&scope=' + scopes

        userEmail = localStorage.getItem("oooEmailAddress")
        if(userEmail)
            url +=  '&login_hint=' + userEmail
         

        signinWebFlow(url)
        return true
      
      else
        console.log("Returning false from check error")
        return false
      
    


    // Once you get 401 this would be called
    // This would get the access token for user. 
    // and than call the method getLatestHistoryId again 
    async function signinWebFlow(url)
        console.log("THE URL ...")
        console.log(url)
        await chrome.identity.launchWebAuthFlow(
            
                'url': url, 
                'interactive':true
            , 
            function(redirectedTo) 
                if (chrome.runtime.lastError) 
                    // Example: Authorization page could not be loaded.
                    console.log(chrome.runtime.lastError.message);
                
                else 
                    var response = redirectedTo.split('#', 2)[1];
                    console.log(response);

                    access_token = getJsonFromUrl(response)["access_token"]
                    console.log(access_token)
                    gapi.auth.setToken(access_token: access_token);
                    getLatestHistoryId()
                
            
        );
    

    // This is to parse the get response 
    // referred from https://***.com/questions/8486099/how-do-i-parse-a-url-query-parameters-in-javascript
    function getJsonFromUrl(query) 
      // var query = location.search.substr(1);
      var result = ;
      query.split("&").forEach(function(part) 
        var item = part.split("=");
        result[item[0]] = decodeURIComponent(item[1]);
      );
      return result;
    

如果您有任何问题,请随时与我联系。我花了好几天时间加入这些点。我不希望其他人也这样做。

【讨论】:

【参考方案2】:

我还在我的 chrome 扩展程序中使用身份 API 进行 google 授权。当我的谷歌令牌过期时,我曾经获得 401 状态。所以我添加了一个检查,如果我收到我的请求的 401 状态响应,那么我将再次授权并获取令牌(它将在后台发生)并继续我的工作。

这是我background.js的一个例子

var authorizeWithGoogle = function() 
    return new Promise(function(resolve, reject) 
        chrome.identity.getAuthToken( 'interactive': true , function(result) 
            if (chrome.runtime.lastError) 
                alert(chrome.runtime.lastError.message);
                return;
            
            if (result) 
                chrome.storage.local.set('token': result, function() 
                    resolve("success");
                );
             else 
                reject("error");
            
        );
    );


function getEmail(emailId) 
    if (chrome.runtime.lastError) 
        alert(chrome.runtime.lastError.message);
        return;
    
    chrome.storage.local.get(["token"], function(data)
        var url = 'https://www.googleapis.com/gmail/v1/users/me/messages/id?alt=json&access_token=' + data.token;
        url = url.replace("id", emailId);
        doGoogleRequest('GET', url, true).then(result => 
            if (200 === result.status) 
                //Do whatever from the result
             else if (401 === result.status) 
                /*If the status is 401, this means that request is unauthorized (token expired in this case). Therefore refresh the token and get the email*/
                refreshTokenAndGetEmail(emailId);
            
        );
    );


function refreshTokenAndGetEmail(emailId) 
    authorizeWithGoogle().then(getEmail(emailId));

我不需要手动一次又一次地登录。 google 令牌在后台自动刷新。

【讨论】:

据我了解,在 401 的情况下,调用 refreshTokenAndGetEmail 内部调用 chrome.identity.getAuthToken ,交互标志为 true。那不会再次产生弹出窗口吗?另外,我尝试运行代码,但似乎我必须进行很多更改。如果可能的话,你能分享一个工作示例吗? 我只分享了我的工作应用程序中的代码。不,它不会一次又一次地显示弹出窗口。

以上是关于GAPI 的 OAuth - 初始登录 Javascript 后避免身份验证和授权的主要内容,如果未能解决你的问题,请参考以下文章

如何删除 Google OAuth2 gapi 事件监听器?

通过 gapi.signin2.render 按钮的 Google OAuth 未在反应应用程序中点击回调

gapi 未定义 - 与 gapi.auth2.init 相关的 Google 登录问题

Google+ 登录:如何注销 - 使用 (gapi.auth.signOut)

VueJs 启动时加载 gapi.auth2

使用gapi.load时,Google API未捕获异常