代码之家  ›  专栏  ›  技术社区  ›  Afsan Abdulali Gujarati

OAuth for GAPI-避免在初次登录JavaScript后进行身份验证和授权

  •  0
  • Afsan Abdulali Gujarati  · 技术社区  · 7 年前

    我已经创建了一个chrome扩展,它可以阅读电子邮件、做一些事情并使用Google客户端的javascript API创建任务。 我正在使用chrome标识进行身份验证和授权。 扩展按预期工作。然而,它不时地要求签名。我想要的是 在后台授权用户 编写脚本,这样在初始身份验证和授权之后,他们就不需要一遍又一遍地执行它了。

    到目前为止我所做的:

    • 我知道我需要一个刷新令牌来避免这种情况。但是,刷新令牌应该在服务器端而不是客户机端进行交换和存储(因为后台脚本在这里执行的是客户机端的工作,所以这不起作用)。
    • 使用gapi.auth.authorize和immediate true。这就产生了外部可视性方面的错误。当我阅读其他内容时,他们建议在服务器内部使用它。我不确定如何在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 ..")
            });
        };
    

    更新: 根据答案中的建议,我对代码做了一些修改。然而,我仍然面临着同样的问题。以下是更新后的代码段

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

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

    2 回复  |  直到 7 年前
        1
  •  0
  •   Pallavi Goyal    7 年前

    我还在我的Chrome扩展中使用Identity API进行Google授权。当我的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));
    }
    

    我不需要反复手动登录。谷歌令牌在后台自动刷新。

        2
  •  0
  •   Afsan Abdulali Gujarati    7 年前

    所以我相信这就是我问题的答案。

    很少有重要的事情要知道

    • 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? )

    1. 转到Google控制台,创建您自己的项目>凭据>创建凭据>OAuthClientID>Web应用程序。在“授权重定向URI”字段的该页上,以https://.chromiumapp.org格式输入重定向URL。如果您不知道chrome扩展ID是什么,请参考( Chrome extension id - how to find it )
    2. 这将生成一个将进入清单文件的客户机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://stackoverflow.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;
        }
    

    如果您有任何问题,请随时与我联系。我已经花了好几天的时间加入这些活动。我不想别人也这么做。