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.
- <script type="text/ecmascript" src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
- <script language="ecmascript" type="text/ecmascript">
- var tokenReq = '<?xml version="1.0" encoding="utf-8"?>';
- tokenReq += '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">';
- tokenReq += ' <soap:Body>';
- tokenReq += ' <GetUpdatedFormDigestInformation xmlns="http://schemas.microsoft.com/sharepoint/soap/" />';
- tokenReq += ' </soap:Body>';
- tokenReq += '</soap:Envelope>';
- // you should set these values according your actual request
- var usr = 'username@yourdomain.onmicrosoft.com';
- var pwd = 'password';
- var siteFullUrl = "https://yourdomain-my.sharepoint.com";
- var loginUrl = siteFullUrl + "/_forms/default.aspx?wa=wsignin1.0";
- var authReq = '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">'
- authReq += ' <s:Header>'
- authReq += ' <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>'
- authReq += ' <a:ReplyTo>'
- authReq += ' <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>'
- authReq += ' </a:ReplyTo>'
- authReq += ' <a:To s:mustUnderstand="1">https://login.microsoftonline.com/extSTS.srf</a:To>'
- authReq += ' <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">'
- authReq += ' <o:UsernameToken>'
- authReq += ' <o:Username>' + usr + '</o:Username>'
- authReq += ' <o:Password>' + pwd + '</o:Password>'
- authReq += ' </o:UsernameToken>'
- authReq += ' </o:Security>'
- authReq += ' </s:Header>'
- authReq += ' <s:Body>'
- 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">'
- authReq += ' <a:EndpointReference>'
- authReq += ' <a:Address>' + loginUrl + '</a:Address>'
- authReq += ' </a:EndpointReference>'
- authReq += ' </wsp:AppliesTo>'
- authReq += ' <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>'
- authReq += ' <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>'
- authReq += ' <t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>'
- authReq += ' </t:RequestSecurityToken>'
- authReq += ' </s:Body>'
- authReq += '</s:Envelope>';
- function startScript() {
- getToken();
- }
- var token;
- // Step 1: we get the token from the STS
- function getToken()
- {
- $.support.cors = true; // enable cross-domain query
- $.ajax({
- type: 'POST',
- data: authReq,
- crossDomain: true, // had no effect, see support.cors above
- contentType: 'application/soap+xml; charset=utf-8',
- url: 'https://login.microsoftonline.com/extSTS.srf',
- dataType: 'xml',
- success: function (data, textStatus, result) {
- // extract the token from the response data
- // 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
- token = $(result.responseText).find("wsse\\:BinarySecurityToken").text();
- getFedAuthCookies();
- },
- error: function (result, textStatus, errorThrown) {
- reportError(result, textStatus, errorThrown);
- }
- });
- }
- // Step 2: "login" using the token provided by STS in step 1
- function getFedAuthCookies()
- {
- $.support.cors = true; // enable cross-domain query
- $.ajax({
- type: 'POST',
- data: token,
- crossDomain: true, // had no effect, see support.cors above
- contentType: 'application/x-www-form-urlencoded',
- url: loginUrl,
- // dataType: 'html', // default is OK: Intelligent Guess (xml, json, script, or html)
- success: function (data, textStatus, result) {
- // we should update the digest
- //refreshDigestViaWS(); // or alternatively:
- refreshDigestViaREST();
- },
- error: function (result, textStatus, errorThrown) {
- reportError(result, textStatus, errorThrown);
- }
- });
- }
- var digest;
- // Step 3a: get the digest from the Sites web service and refresh the one stored locally
- function refreshDigestViaWS()
- {
- $.support.cors = true; // enable cross-domain query
- $.ajax({
- type: 'POST',
- data: tokenReq,
- crossDomain: true, // had no effect, see support.cors above
- contentType: 'text/xml; charset="utf-8"',
- url: siteFullUrl + '/_vti_bin/sites.asmx',
- headers: {
- 'SOAPAction': 'http://schemas.microsoft.com/sharepoint/soap/GetUpdatedFormDigestInformation',
- 'X-RequestForceAuthentication': 'true'
- },
- dataType: 'xml',
- success: function (data, textStatus, result) {
- digest = $(result.responseXML).find("DigestValue").text();
- sendRESTReq();
- },
- error: function (result, textStatus, errorThrown) {
- var response = JSON.parse(result.responseText);
- if ((response.error != undefined) && (response.error.message != undefined)) {
- alert(response.error.message.value);
- }
- }
- });
- }
- // Step 3b: get the digest from the contextinfo and refresh the one stored locally
- function refreshDigestViaREST()
- {
- $.support.cors = true; // enable cross-domain query
- $.ajax({
- type: 'POST',
- data: tokenReq,
- crossDomain: true, // had no effect, see support.cors above
- contentType: 'text/xml; charset="utf-8"',
- url: siteFullUrl + '/_api/contextinfo',
- dataType: 'xml',
- success: function (data, textStatus, result) {
- digest = $(result.responseText).find("d\\:FormDigestValue").text();
- sendRESTReq();
- },
- error: function (result, textStatus, errorThrown) {
- var response = JSON.parse(result.responseText);
- if ((response.error != undefined) && (response.error.message != undefined)) {
- alert(response.error.message.value);
- }
- }
- });
- }
- // Step 4: send the REST request
- function sendRESTReq() {
- $.support.cors = true; // enable cross-domain query
- $.ajax({
- type: 'POST',
- data: JSON.stringify({
- __metadata: { type: 'SP.List' },
- Title: 'RESTDocLib',
- BaseTemplate: 101
- }),
- // equivalent:
- // data: "{'__metadata': { 'type': 'SP.List' }, 'Title': 'RESTDocLib','BaseTemplate': 101}" ,
- url: siteFullUrl + "/_api/web/lists",
- crossDomain: true, // had no effect, see support.cors above
- contentType: 'application/json;odata=verbose',
- headers: {
- 'X-RequestDigest': digest,
- "Accept": "application/json; odata=verbose"
- },
- success: function (data, textStatus, result) {
- alert("Created");
- },
- error: function (result, textStatus, errorThrown) {
- var response = JSON.parse(result.responseText);
- if ((response.error != undefined) && (response.error.message != undefined)) {
- alert(response.error.message.value);
- }
- }
- });
- }
- function reportError(result, textStatus, errorThrown) {
- var response = JSON.parse(result.responseText);
- if ((response.error != undefined) && (response.error.message != undefined)) {
- alert(response.error.message.value);
- }
- }
- $(document).ready(startScript);
- </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.
