Browser to Xero - Part 2 - OAuth

Last time we created a simple HTTP proxy, that supports CORS, so we can access the Xero API directly from a browser. This time we'll be navigating the Xero OAuth labyrinth to make sure we have the required API access. We'll start by registering our website with Xero and follow by implementing the RequestToken-Login-AccessToken process.

The OAuth Labyrinth

Our test web page will contain a single button to initiate the process. Once clicked, the browser will jump to a Xero page whereby a user can allow access. Once we allow access, the Xero page will jump back to our test page and our test page will show a simple message. Behind the scenes, the process is a little more involved. The steps are shown below.

Xero Setup

Now, before any of this can happen, we must create a Xero account and register our "App" by supplying an App name, application URL, and callback domain. The App name is arbitrary, however, the Application URL must point to our previously created proxy since it is what will ultimately be communicating with Xero. In my case, I'll be running it locally so I enter "http://localhost:8080". The callback domain is used when Xero returns control to your UI web page, which I will also be running locally, hence I enter "localhost". Once completed we are supplied with a Consumer Key and a Consumer Secret. These we will need to incorporate into our test web page.

Dependencies

I usually encourage folk to build their own stuff rather than relying on third party products for everything. This is not one of those occasions. This time I'll be making use of oauth-1.0a and crypto-js. To do that we need to incorporate our Xero Consumer Key and Consumer Secret into the following object:

1:  var oauth_details =  
2:  {  
3:    consumer:  
4:    {  
5:       key: "insert consumer key here",   
6:       secret: "insert consumer secret here"   
7:    },  
8:    signature_method: "HMAC-SHA1",  
9:    hash_function: function (base_string, key)  
10:   {  
11:     return CryptoJS.HmacSHA1(base_string, key).toString(CryptoJS.enc.Base64);  
12:   }  
13: };  

Request Token

So let's get started with the code. Our "app" consists of a single HTML page with a button to initiate the access process, and a DIV to display a message or two (full listing at the end of the article). When the user clicks the button, we call the Get_Xero_Access_On_Click() function. This function then calls our Get_Request_Token() function passing it the URL of our test page. If the request completed successfully we take note of the OAuth Token and Secret and redirect to the Xero access authorisation page. When the user approves our access, Xero will redirect back to our test page.

1:  function Get_Xero_Access_On_Click()  
2:  {  
3:    Get_Request_Token(web_host + "/xero_test.html", Request_Token_Received);  
4:    function Request_Token_Received(res_text)  
5:    {  
6:      var url, oauth_token, oauth_token_secret;  
7:    
8:      oauth_token = Get_Query_Value(res_text, "oauth_token");  
9:      oauth_token_secret = Get_Query_Value(res_text, "oauth_token_secret");  
10:     if (oauth_token !== null && oauth_token_secret !== null)  
11:     {  
12:       localStorage.oauth_token_secret = oauth_token_secret;  
13:       url = xero_host + "/oauth/Authorize?oauth_token=" + oauth_token + "&oauth_token_secret=" + oauth_token_secret;  
14:       window.open(url, "_self");  
15:     }  
16:   }  
17: }  

Access Token

Everytime our test page loads, we check to see if we've received an OAuth Token and Verifier and, if we did, we assume this is because a Xero user has approved access and Xero has redirected to this page. With the token and verifier, we can call our Get_Access_Token() function and if all goes well we are finally ready to start interacting with the Xero API. Easy! Like drilling a hole in one's head.

 1: window.onload = Main;  
 2: function Main()  
 3: {  
 4:   var oauth_token, oauth_verifier;  
 5:    
 6:   oauth_token = Get_Query_Value(window.location.href, "oauth_token");  
 7:   oauth_verifier = Get_Query_Value(window.location.href, "oauth_verifier");  
 8:   if (oauth_token !== null && oauth_verifier !== null)  
 9:   {  
10:     Get_Access_Token(oauth_token, oauth_verifier, Access_Token_Received);  
11:     function Access_Token_Received(res_text)  
12:     {  
13:       var oauth_token_secret, oauth_token;  
14:    
15:       oauth_token = Get_Query_Value(res_text, "oauth_token");  
16:       oauth_token_secret = Get_Query_Value(res_text, "oauth_token_secret");  
17:       if (oauth_token !== null && oauth_token_secret !== null)  
18:         Get_Xero_Data();  
19:     }  
20:   }  
21: }  

Loose Ends

Before concluding this article, one should take note that a suitable web and proxy host will have to be specified. I've been using Visual Studio 2017 with IIS running locally so my test page website address was "http://localhost:50107". I also wrote and ran my proxy locally with Eclipse Neon and Google App Engine so my proxy address was "http://localhost:8080". There's no reason why you couldn't run either of these remotely. Also, we need to consider security. All our Consumer details and tokens have been stored or hard-coded on the client HTML page. NOT VERY SECURE. A better approach would be to hide them in the proxy. Next time we'll get started with the Xero API itself.

The Code

1:  <!DOCTYPE html>  
2:  <html>  
3:    
4:   <head>  
5:    <meta charset="utf-8" />  
6:    <title>Xero Test</title>  
7:    
8:    <script src="/libs/CryptoJS v3.1.2/rollups/hmac-sha1.js"></script>  
9:    <script src="/libs/CryptoJS v3.1.2/rollups/hmac-sha256.js"></script>  
10:    <script src="/libs/CryptoJS v3.1.2/components/enc-base64-min.js"></script>  
11:    <script src="/libs/oauth-1.0a.js"></script>  
12:    <script>  
13:    
14:     var web_host = "http://localhost:50107";  
15:     var proxy_host = "http://localhost:8080";  
16:     var xero_host = "https://api.xero.com";  
17:     var oauth_details =  
18:     {  
19:      consumer:  
20:      {  
21:       key: "MT06IAEFZLDM49ZCMKSYBGQRMVWDTW",   
22:       secret: "I95LWGXQHRZ4U1WIUJDSXXXXQ0GWEY"   
23:      },  
24:      signature_method: "HMAC-SHA1",  
25:      hash_function: function (base_string, key)  
26:      {  
27:       return CryptoJS.HmacSHA1(base_string, key).toString(CryptoJS.enc.Base64);  
28:      }  
29:     };  
30:     window.onload = Main;  
31:    
32:     function Main()  
33:     {  
34:      var oauth_token, oauth_verifier;  
35:    
36:      oauth_token = Get_Query_Value(window.location.href, "oauth_token");  
37:      oauth_verifier = Get_Query_Value(window.location.href, "oauth_verifier");  
38:      if (oauth_token !== null && oauth_verifier !== null)  
39:      {  
40:       Get_Access_Token(oauth_token, oauth_verifier, Access_Token_Received);  
41:       function Access_Token_Received(res_text)  
42:       {  
43:        var oauth_token_secret, oauth_token;  
44:    
45:        oauth_token = Get_Query_Value(res_text, "oauth_token");  
46:        oauth_token_secret = Get_Query_Value(res_text, "oauth_token_secret");  
47:        if (oauth_token !== null && oauth_token_secret !== null)  
48:         Get_Xero_Data();  
49:       }  
50:      }  
51:     }  
52:    
53:     function Get_Xero_Data()  
54:     {  
55:      document.getElementById("msg").textContent = "We have lift-off!";  
56:     }  
57:    
58:     function Get_Xero_Access_On_Click()  
59:     {  
60:      Get_Request_Token(web_host + "/xero_test.html", Request_Token_Received);  
61:      function Request_Token_Received(res_text)  
62:      {  
63:       var url, oauth_token, oauth_token_secret;  
64:    
65:       oauth_token = Get_Query_Value(res_text, "oauth_token");  
66:       oauth_token_secret = Get_Query_Value(res_text, "oauth_token_secret");  
67:       if (oauth_token !== null && oauth_token_secret !== null)  
68:       {  
69:        localStorage.oauth_token_secret = oauth_token_secret;  
70:        url = xero_host + "/oauth/Authorize?oauth_token=" + oauth_token + "&oauth_token_secret=" + oauth_token_secret;  
71:        window.open(url, "_self");  
72:       }  
73:      }  
74:     }  
75:    
76:     function Get_Access_Token(oauth_token, oauth_verifier, success_fn)  
77:     {  
78:      var req_data, hds, token_url, oauth;  
79:    
80:      token_url = "/oauth/AccessToken";  
81:    
82:      oauth = OAuth(oauth_details);  
83:      req_data =  
84:      {  
85:       url: xero_host + token_url,  
86:       method: "POST",  
87:       data: { oauth_verifier: oauth_verifier }  
88:      };  
89:      hds = oauth.toHeader(oauth.authorize(req_data, {key: oauth_token, secret: localStorage.oauth_token_secret}));  
90:    
91:      Send_Request(proxy_host + token_url, hds.Authorization, success_fn);  
92:     }  
93:    
94:     function Get_Request_Token(oauth_callback_url, success_fn)  
95:     {  
96:      var req_data, hds, token_url, oauth;  
97:    
98:      token_url = "/oauth/RequestToken";  
99:    
100:      oauth = OAuth(oauth_details);  
101:      req_data =  
102:      {  
103:       url: xero_host + token_url,  
104:       method: "POST",  
105:       data: { oauth_callback: oauth_callback_url }  
106:      };  
107:      hds = oauth.toHeader(oauth.authorize(req_data));  
108:    
109:      Send_Request(proxy_host + token_url, hds.Authorization, success_fn);  
110:     }  
111:    
112:     function Send_Request(url, auth_hdr, success_fn)  
113:     {  
114:      var req;  
115:    
116:      req = new XMLHttpRequest();  
117:      req.onreadystatechange = Req_State_Change;  
118:      req.open("POST", url, true);  
119:      req.setRequestHeader("Authorization", auth_hdr);  
120:      req.send();  
121:      function Req_State_Change()  
122:      {  
123:       if (this.readyState === XMLHttpRequest.DONE)  
124:        success_fn(this.responseText);  
125:      }  
126:     }  
127:    
128:     function Get_Query_Value(query, field_name)  
129:     {  
130:      var field_value = null;  
131:    
132:      start_pos = query.indexOf(field_name + "=");  
133:      if (start_pos !== -1)  
134:      {  
135:       start_pos += field_name.length + 1;  
136:       end_pos = query.indexOf("&", start_pos);  
137:       if (end_pos !== -1)  
138:        field_value = query.substring(start_pos, end_pos);  
139:       else  
140:        field_value = query.substring(start_pos);  
141:      }  
142:      return field_value;  
143:     }  
144:    </script>  
145:   </head>  
146:    
147:   <body>  
148:    <div id="msg"></div>  
149:    <button onclick="Get_Xero_Access_On_Click()">Get Xero Access</button>  
150:   </body>  
151:    
152:  </html>  

Popular Posts