Second Life of a Hungarian SharePoint Geek

May 11, 2013

Accessing Office 365 SharePoint sites using REST from a local HTML / JavaScript Host

Filed under: JavaScript, jQuery, Office 365, REST, SP 2013 — Tags: , , , , — Peter Holpar @ 22:12

A few month ago I wrote about accessing Office 365 sites using the JavaScript. In that sample I used the SharePoint ECMAScript client object model. Last month a commenter, Gilles asked if we could use REST the same way to access the information on the O365 site. The answer is a definitive yes, I already provided similar solutions in my Favorites in the Cloud posts (here and here). In those posts I used however WinJS.xhr and not the ajax method of jQuery, so it might worth to see another sample that utilizes jQuery.

Originally – to keep the samples in this post short – I planned to include only the code that is relevant to the solution and / or differs from the client OM solution, however later I made a lot of small enhancements in the original code as well, so probably it is simpler to publish the full code “as is” even with possible duplicates / overlapping with the former version.

The sample in this post simply creates a document library in the O365 site, but it illustrates the process of authentication and can serve as a base for more sophisticated applications as well.

The format of the token (tokenReq) and the authentication requests (authReq) is the same as the JSCOM sample, and the process itself is also very similar:

1. We get the token from the security token service (STS) of MS Online.

2. "Login" to the actual O365 site using the token provided by STS in the former step. As a result of this step, we have the required cookies (FedAuth and rtFA) to be used automatically in the next steps. These cookies are set by Set-Cookie headers of the response and cached and reused by the browser for later requests targeting the same site.

3. Get the digest from the contextinfo REST endpoint (see MSDN for details) or from the Sites web service store the value into a JavaScript variable (digest).

4. Execute the REST request.

Code Snippet
  1. <script type="text/ecmascript" src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
  2.  
  3. <script language="ecmascript" type="text/ecmascript">
  4.  
  5.     var tokenReq = '<?xml version="1.0" encoding="utf-8"?>';
  6.     tokenReq += '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance&quot; xmlns:xsd="http://www.w3.org/2001/XMLSchema&quot; xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">&#039;;
  7.     tokenReq += '  <soap:Body>';
  8.     tokenReq += '    <GetUpdatedFormDigestInformation xmlns="http://schemas.microsoft.com/sharepoint/soap/&quot; />';
  9.     tokenReq += '  </soap:Body>';
  10.     tokenReq += '</soap:Envelope>';
  11.  
  12.     // you should set these values according your actual request
  13.     var usr = 'username@yourdomain.onmicrosoft.com';
  14.     var pwd = 'password';
  15.     var siteFullUrl = "https://yourdomain-my.sharepoint.com&quot;;
  16.    
  17.     var loginUrl = siteFullUrl + "/_forms/default.aspx?wa=wsignin1.0";
  18.     var authReq =   '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope&quot; xmlns:a="http://www.w3.org/2005/08/addressing&quot; xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">&#039;
  19.     authReq +=      '  <s:Header>'
  20.     authReq +=      '    <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>&#039;
  21.     authReq +=      '    <a:ReplyTo>'
  22.     authReq +=      '      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>&#039;
  23.     authReq +=      '    </a:ReplyTo>'
  24.     authReq +=      '    <a:To s:mustUnderstand="1">https://login.microsoftonline.com/extSTS.srf</a:To>&#039;
  25.     authReq +=      '    <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">&#039;
  26.     authReq +=      '      <o:UsernameToken>'
  27.     authReq +=      '        <o:Username>' + usr + '</o:Username>'
  28.     authReq +=      '        <o:Password>' + pwd + '</o:Password>'
  29.     authReq +=      '      </o:UsernameToken>'
  30.     authReq +=      '    </o:Security>'
  31.     authReq +=      '  </s:Header>'
  32.     authReq +=      '  <s:Body>'
  33.     authReq +=      '    <t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust"><wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">&#039;
  34.     authReq +=      '      <a:EndpointReference>'
  35.     authReq +=      '        <a:Address>' + loginUrl + '</a:Address>'
  36.     authReq +=      '      </a:EndpointReference>'
  37.     authReq +=      '      </wsp:AppliesTo>'
  38.     authReq +=      '      <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>&#039;
  39.     authReq +=      '      <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>&#039;
  40.     authReq +=      '      <t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>'
  41.     authReq +=      '    </t:RequestSecurityToken>'
  42.     authReq +=      '  </s:Body>'
  43.     authReq +=      '</s:Envelope>';    
  44.  
  45.     function startScript() {
  46.       getToken();
  47.     }
  48.  
  49.     var token;
  50.     // Step 1: we get the token from the STS
  51.     function getToken()
  52.     {
  53.         $.support.cors = true; // enable cross-domain query
  54.         $.ajax({
  55.             type: 'POST',
  56.             data: authReq,
  57.             crossDomain: true, // had no effect, see support.cors above
  58.             contentType: 'application/soap+xml; charset=utf-8',
  59.             url: 'https://login.microsoftonline.com/extSTS.srf&#039;,
  60.             dataType: 'xml',
  61.             success: function (data, textStatus, result) {
  62.                 // extract the token from the response data
  63.                 // var token = $(result.responseXML).find("wsse\\:BinarySecurityToken").text(); // we should work with responseText, because responseXML is undefined, due to Content-Type: application/soap+xml; charset=utf-8
  64.                 token = $(result.responseText).find("wsse\\:BinarySecurityToken").text();
  65.                 getFedAuthCookies();
  66.             },
  67.             error: function (result, textStatus, errorThrown) {
  68.                 reportError(result, textStatus, errorThrown);
  69.             }
  70.         });
  71.     }
  72.  
  73.     // Step 2: "login" using the token provided by STS in step 1
  74.     function getFedAuthCookies()
  75.     {
  76.         $.support.cors = true; // enable cross-domain query
  77.         $.ajax({
  78.             type: 'POST',
  79.             data: token,
  80.             crossDomain: true, // had no effect, see support.cors above
  81.             contentType: 'application/x-www-form-urlencoded',
  82.             url: loginUrl,         
  83.             // dataType: 'html', // default is OK: Intelligent Guess (xml, json, script, or html)
  84.             success: function (data, textStatus, result) {
  85.                 // we should update the digest
  86.                 //refreshDigestViaWS(); // or alternatively:
  87.                 refreshDigestViaREST();
  88.             },
  89.             error: function (result, textStatus, errorThrown) {
  90.                 reportError(result, textStatus, errorThrown);
  91.             }
  92.         });
  93.     }
  94.  
  95.     var digest;
  96.  
  97.     // Step 3a: get the digest from the Sites web service and refresh the one stored locally
  98.     function refreshDigestViaWS()
  99.     {
  100.         $.support.cors = true; // enable cross-domain query
  101.         $.ajax({
  102.             type: 'POST',
  103.             data: tokenReq,
  104.             crossDomain: true, // had no effect, see support.cors above
  105.             contentType: 'text/xml; charset="utf-8"',
  106.             url: siteFullUrl + '/_vti_bin/sites.asmx',
  107.             headers: {
  108.                 'SOAPAction': 'http://schemas.microsoft.com/sharepoint/soap/GetUpdatedFormDigestInformation&#039;,
  109.                 'X-RequestForceAuthentication': 'true'
  110.             },
  111.             dataType: 'xml',
  112.             success: function (data, textStatus, result) {
  113.                 digest = $(result.responseXML).find("DigestValue").text();
  114.                 sendRESTReq();
  115.             },
  116.             error: function (result, textStatus, errorThrown) {
  117.                         var response = JSON.parse(result.responseText);
  118.                         if ((response.error != undefined) && (response.error.message != undefined)) {
  119.                             alert(response.error.message.value);
  120.                         }
  121.             }
  122.         });
  123.     }
  124.  
  125.     // Step 3b: get the digest from the contextinfo and refresh the one stored locally
  126.     function refreshDigestViaREST()
  127.     {
  128.         $.support.cors = true; // enable cross-domain query
  129.         $.ajax({
  130.             type: 'POST',
  131.             data: tokenReq,
  132.             crossDomain: true, // had no effect, see support.cors above
  133.             contentType: 'text/xml; charset="utf-8"',
  134.             url: siteFullUrl + '/_api/contextinfo',
  135.             dataType: 'xml',
  136.             success: function (data, textStatus, result) {  
  137.                 digest = $(result.responseText).find("d\\:FormDigestValue").text();
  138.                 sendRESTReq();
  139.             },
  140.             error: function (result, textStatus, errorThrown) {
  141.                         var response = JSON.parse(result.responseText);
  142.                         if ((response.error != undefined) && (response.error.message != undefined)) {
  143.                             alert(response.error.message.value);
  144.                         }
  145.             }
  146.         });
  147.     }
  148.  
  149.     // Step 4: send the REST request
  150.     function sendRESTReq() {
  151.         $.support.cors = true; // enable cross-domain query
  152.         $.ajax({
  153.             type: 'POST',   
  154.             data: JSON.stringify({
  155.                                     __metadata: { type: 'SP.List' },
  156.                                     Title: 'RESTDocLib',
  157.                                     BaseTemplate: 101
  158.                                 }),
  159.             // equivalent:       
  160.             // data: "{'__metadata': { 'type': 'SP.List' }, 'Title': 'RESTDocLib','BaseTemplate': 101}" ,
  161.             url: siteFullUrl + "/_api/web/lists",
  162.             crossDomain: true, // had no effect, see support.cors above
  163.             contentType: 'application/json;odata=verbose',
  164.              headers: {
  165.                  'X-RequestDigest': digest,
  166.                  "Accept": "application/json; odata=verbose"
  167.              },
  168.             success: function (data, textStatus, result) {  
  169.                 alert("Created");
  170.             },
  171.             error: function (result, textStatus, errorThrown) {
  172.                         var response = JSON.parse(result.responseText);
  173.                         if ((response.error != undefined) && (response.error.message != undefined)) {
  174.                             alert(response.error.message.value);
  175.                         }
  176.             }
  177.         });
  178.     }
  179.  
  180.     function reportError(result, textStatus, errorThrown) {
  181.         var response = JSON.parse(result.responseText);
  182.         if ((response.error != undefined) && (response.error.message != undefined)) {
  183.             alert(response.error.message.value);
  184.         }
  185.     }
  186.  
  187.     $(document).ready(startScript);
  188.  
  189. </script>

Note 1: The lookup of the token (in the getToken method) had to be changed. In the former version we used:

$(result.responseText).find("BinarySecurityToken").text();

in the new version we have to use:

$(result.responseText).find("wsse\\:BinarySecurityToken").text();

Note 2: I used JSON’s stringify and parse methods to (de)serialize JavaScript objects to / from text. Important experience, that this methods do not work in the Quirks mode of Internet Explorer.

Advertisements

2 Comments »

  1. What is up with the user name you supply: var usr = ‘username@yourdomain.onmicrosoft.com’;? When I log into Office 365 for my business it asks for a username like “Domain\Username” should I use that? If not how do I figure out what my username is? For reference the siteFullUrl I want to log into is: “https://pcdhc-my.sharepoint.com”.

    Comment by Joel — January 13, 2015 @ 17:50

  2. Really appreciate the code which works fine for the most of the people . the issue i am facing when using the same code on one of my sharepoint public site. Unable to make the ajax call for microsoft online service for Login. Getting error as – Origin –//mysite//– not found i n access -control-allow-origin header. xmlhttprequest access denied.

    Comment by dheeraj — July 1, 2015 @ 08:25


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: