How to download a file version from office 365 using csom

I need to download an older file version from office 365 and get the data into a byte array. I have no trouble downloading the latest version with File.OpenBinaryStream() and I have no trouble loading the previous file versions with File.Versions. But now I need to actually download an older version of the file and it seems the only way is to use File.OpenBinaryDirect. So I am creating a client context using my oAuth access token and providing the correct path, but I am getting a (401) Unauthorized error. Looking with Fiddler I can see that the call to OpenBinaryDirect is somehow trying to post to my file URL and the server is responding with 401.

context = TokenHelper.GetClientContextWithAccessToken(SPHostUrl, AccessToken);

FileInformation info = File.OpenBinaryDirect(context, "/" + _fileVersion.Url);  //throws 401

//leading slash required otherwise ArgumentOutOfRangeException

I have to be able to access the older file versions with my c# code -- I don't have a viable app without that ability -- any help urgently needed and greatly appreciated!



  • Edited by pmott Thursday, February 20, 2014 8:18 PM
February 20th, 2014 8:10pm

If just browse the url of the file version, can you browse? I'm just wondering if the file version url is wrong? As I can remember the file version url looks like '_vti_history/versionno/filename'..
Free Windows Admin Tool Kit Click here and download it now
February 21st, 2014 6:41am

In my case _fileVersion.Url is "_vti_history/512/Lists/Forms/repeattest2.xsn"

When I use a browser to navigate to {SPAppWebUrl}/_vti_history/512/Lists/Forms/repeattest2.xsn the file gets downloaded without any trouble.

I'm pretty desperate here so I am trying everything I can think of. One thing I thought of, since FIle.OpenBinaryStream() works OK,  is this:

Web web = context.Web;
context.Load(web);
context.ExecuteQuery();

File file = web.GetFileByServerRelativeUrl(_fileVersion.Url);
context.Load(file);
context.ExecuteQuery();

But this crashes with "Specified value is not supported for the serverRelativeUrl parameter."

Surely there must be some way to download a previous version with a provider hosted SharePoint app, but after two days I cannot find a single example anywhere.  Help!



  • Edited by pmott Friday, February 21, 2014 6:33 PM
February 21st, 2014 6:20pm

I can see in fiddler that the browser sends the FedAuth cookie when I download manually, but using the ClientContext created with an AccessToken does not.

So it looks like one way to get this done is described here: http://blogs.msdn.com/b/cjohnson/archive/2011/05/14/part-2-headless-authentication-with-sharepoint-online-and-the-client-side-object-model.aspx

This should work but I still have the problem of username/password. The user is already logged into O365 and my app has an AccessToken -- I'm wondering if there is some way to use that in place of username/password in the "headless" example?

 
Free Windows Admin Tool Kit Click here and download it now
February 24th, 2014 7:53pm

Did you ever accomplish this?  I am stuck as well (http://social.msdn.microsoft.com/Forums/en-US/ce1d1b07-023c-485b-ae78-468262ebfeb1/provider-hosted-app-how-can-i-download-an-old-version-of-a-file?forum=appsforsharepoint)

March 11th, 2014 5:05pm

I was at SPC2014 last week and met with a product manager about this. Currently there is no way to do it with an access token -- but they are aware of the problem and plan to add in the next CSOM update.

In the meantime we have used the MsOnlineClaimsHelper class to download directly from the url that is available now. We dedicate an account and store the credentials in web.config to be used when needed.

  • Proposed as answer by DPSICorp Tuesday, March 11, 2014 6:31 PM
Free Windows Admin Tool Kit Click here and download it now
March 11th, 2014 5:23pm

Thank you SO much (Can't wait for the next release)!

For anyone else who lands here, here's the code I ended up using:

// VersionAccessUser and VersionAccessPassword are stored in web.config
// web.Url is loaded via the clientContext
// myVersion is the FileVersion I got from the file's Versions.GetById() method
// probably a lot of ways to get hostUrl, it just needs to be https://yourdomain.sharepoint.com/ 
// - I'm running my app from a subweb
// I had trouble following the links to get the full MsOnlineClaimsHelper code 
// (the one on msdn.com was missing RequestBodyWriter, WSTrustFeb2005ContractClient,
// and IWSTrustFeb2005Contract
// so I've included the code I used here.


string myVersionFullUrl = string.Format("{0}/{1}", web.Url, myVersion.Url);
string userName = WebConfigurationManager.AppSettings.Get("VersionAccessUser");
string strPassword = WebConfigurationManager.AppSettings.Get("VersionAccessPassword");
string hostUrl = Regex.Replace(web.Url, "([^/]+//[^/]+/).*", "$1");
MsOnlineClaimsHelper claimsHelper = new MsOnlineClaimsHelper(hostUrl, userName, strPassword);
var client = new WebClient();
client.Headers["Accept"] = "/";
client.Headers.Add(HttpRequestHeader.UserAgent, "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)");
client.Headers.Add(HttpRequestHeader.Cookie, claimsHelper.CookieContainer.GetCookieHeader(new Uri(hostUrl)));
var document = client.DownloadString(myVersionFullUrl);

// ***************************************************************************************** //
// These classes are needed to download old versions of files (see: http://social.msdn.microsoft.com/Forums/en-US/7746d857-d351-49cc-b2f0-496663239e02/how-to-download-a-file-version-from-office-365-using-csom?forum=sharepointdevelopment)
// I cobbled this file from http://social.technet.microsoft.com/Forums/msonline/en-US/4e304493-7ddd-4721-8f46-cb7875078f8b/problem-logging-in-to-office-365-sharepoint-online-from-webole-hosted-in-the-cloud?forum=onlineservicessharepoint
// and http://fredericloud.com/2011/01/11/connecting-to-sharepoint-with-claims-authentication/


using Microsoft.IdentityModel.Protocols.WSTrust;
using Microsoft.SharePoint.Client;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;
using System.Web;
using System.Xml;
using System.Xml.Linq;

namespace DPSiDoxAppWeb.Helpers
{
    /// <summary>
    /// Create a new contract to use for issue claims for the SharePoint requests
    /// </summary>
    [ServiceContract]
    public interface IWSTrustFeb2005Contract
    {
        [OperationContract(ProtectionLevel = ProtectionLevel.EncryptAndSign,
            Action = "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue",
            ReplyAction = "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/Issue",
            AsyncPattern = true)]
        IAsyncResult BeginIssue(Message request, AsyncCallback callback, object state);
        Message EndIssue(IAsyncResult asyncResult);
    }

    /// <summary>
    /// Implement the client contract for the new type
    /// </summary>
    public class WSTrustFeb2005ContractClient : ClientBase<IWSTrustFeb2005Contract>, IWSTrustFeb2005Contract
    {
        public WSTrustFeb2005ContractClient(Binding binding, EndpointAddress remoteAddress)
            : base(binding, remoteAddress)
        { }

        public IAsyncResult BeginIssue(Message request, AsyncCallback callback, object state)
        {
            return Channel.BeginIssue(request, callback, state);
        }

        public Message EndIssue(IAsyncResult asyncResult)
        {
            return Channel.EndIssue(asyncResult);
        }
    }

    /// <summary>
    /// Create a class that will serialize the token into the request
    /// </summary>
    class RequestBodyWriter : BodyWriter
    {
        readonly WSTrustRequestSerializer _serializer;
        readonly RequestSecurityToken _rst;

        /// <summary>
        /// Constructs the Body Writer.
        /// </summary>
        /// <param name="serializer">Serializer to use for serializing the rst.</param>
        /// <param name="rst">The RequestSecurityToken object to be serialized to the outgoing Message.</param>
        public RequestBodyWriter(WSTrustRequestSerializer serializer, RequestSecurityToken rst)
            : base(false)
        {
            if (serializer == null)
                throw new ArgumentNullException("serializer");

            _serializer = serializer;
            _rst = rst;
        }


        /// <summary>
        /// Override of the base class method. Serializes the rst to the outgoing stream.
        /// </summary>
        /// <param name="writer">Writer to which the rst should be written.</param>
        protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
        {
            _serializer.WriteXml(_rst, writer, new WSTrustSerializationContext());
        }
    }

    public class MsOnlineClaimsHelper
    {
        #region Properties

        readonly string _username;
        readonly string _password;
        readonly bool _useRtfa;
        readonly Uri _host;

        CookieContainer _cachedCookieContainer = null;
        DateTime _expires = DateTime.MinValue;

        #endregion

        #region Constructors
        public MsOnlineClaimsHelper(string host, string username, string password)
            : this(new Uri(host), username, password)
        {
        }

        public MsOnlineClaimsHelper(Uri host, string username, string password)
        {
            _host = host;
            _username = username;
            _password = password;
            _useRtfa = true;
        }

        public MsOnlineClaimsHelper(Uri host, string username, string password, bool useRtfa)
        {
            _host = host;
            _username = username;
            _password = password;
            _useRtfa = useRtfa;
        }
        #endregion

        #region Constants
        public const string office365STS = "https://login.microsoftonline.com/extSTS.srf";
        public const string office365Login = "https://login.microsoftonline.com/login.srf";
        public const string office365Metadata = "https://nexus.microsoftonline-p.com/federationmetadata/2007-06/federationmetadata.xml";
        public const string wsse = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
        public const string wsu = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
        private const string userAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)";
        #endregion

        class MsoCookies
        {
            public string FedAuth { get; set; }
            public string rtFa { get; set; }

            public DateTime Expires { get; set; }
            public Uri Host { get; set; }
        }

        // Method used to add cookies to CSOM
        public void clientContext_ExecutingWebRequest(object sender, WebRequestEventArgs e)
        {
            e.WebRequestExecutor.WebRequest.CookieContainer = getCookieContainer();
            //e.WebRequestExecutor.WebRequest.UserAgent = userAgent;
        }

        // Creates or loads cached cookie container
        CookieContainer getCookieContainer()
        {
            if (_cachedCookieContainer == null || DateTime.Now > _expires)
            {
                // Get the SAML tokens from SPO STS (via MSO STS) using fed auth passive approach
                MsoCookies cookies = getSamlToken();

                if (cookies != null && !string.IsNullOrEmpty(cookies.FedAuth))
                {
                    // Create cookie collection with the SAML token          
                    _expires = cookies.Expires;
                    CookieContainer cc = new CookieContainer();

                    // Set the FedAuth cookie
                    Cookie samlAuth = new Cookie("FedAuth", cookies.FedAuth)
                    {
                        Expires = cookies.Expires,
                        Path = "/",
                        Secure = cookies.Host.Scheme == "https",
                        HttpOnly = true,
                        Domain = cookies.Host.Host
                    };
                    cc.Add(samlAuth);


                    if (_useRtfa)
                    {
                        // Set the rtFA (sign-out) cookie, added march 2011
                        Cookie rtFa = new Cookie("rtFA", cookies.rtFa)
                        {
                            Expires = cookies.Expires,
                            Path = "/",
                            Secure = cookies.Host.Scheme == "https",
                            HttpOnly = true,
                            Domain = cookies.Host.Host
                        };
                        cc.Add(rtFa);
                    }
                    _cachedCookieContainer = cc;
                    return cc;
                }
                return null;
            }
            return _cachedCookieContainer;
        }

        public CookieContainer CookieContainer
        {
            get
            {
                if (_cachedCookieContainer == null || DateTime.Now > _expires)
                {
                    return getCookieContainer();
                }
                return _cachedCookieContainer;
            }
        }

        private MsoCookies getSamlToken()
        {
            MsoCookies ret = new MsoCookies();

            try
            {
                var sharepointSite = new
                {
                    Wctx = office365Login,
                    Wreply = _host.GetLeftPart(UriPartial.Authority) + "/_forms/default.aspx?wa=wsignin1.0"
                };

                //get token from STS
                string stsResponse = getResponse(office365STS, sharepointSite.Wreply);

                // parse the token response
                XDocument doc = XDocument.Parse(stsResponse);

                // get the security token
                var crypt = from result in doc.Descendants()
                            where result.Name == XName.Get("BinarySecurityToken", wsse)
                            select result;

                // get the token expiration
                var expires = from result in doc.Descendants()
                              where result.Name == XName.Get("Expires", wsu)
                              select result;
                ret.Expires = Convert.ToDateTime(expires.First().Value);

                HttpWebRequest request = createRequest(sharepointSite.Wreply);
                byte[] data = Encoding.UTF8.GetBytes(crypt.FirstOrDefault().Value);
                using (Stream stream = request.GetRequestStream())
                {
                    stream.Write(data, 0, data.Length);
                    stream.Close();

                    using (HttpWebResponse webResponse = request.GetResponse() as HttpWebResponse)
                    {

                        // Handle redirect, added may 2011 for P-subscriptions
                        if (webResponse.StatusCode == HttpStatusCode.MovedPermanently)
                        {
                            HttpWebRequest request2 = createRequest(webResponse.Headers["Location"]);
                            using (Stream stream2 = request2.GetRequestStream())
                            {
                                stream2.Write(data, 0, data.Length);
                                stream2.Close();

                                using (HttpWebResponse webResponse2 = request2.GetResponse() as HttpWebResponse)
                                {
                                    ret.FedAuth = webResponse2.Cookies["FedAuth"].Value;
                                    ret.rtFa = webResponse2.Cookies["rtFa"].Value;
                                    ret.Host = request2.RequestUri;
                                }
                            }
                        }
                        else
                        {
                            ret.FedAuth = webResponse.Cookies["FedAuth"].Value;
                            ret.rtFa = webResponse.Cookies["rtFa"].Value;
                            ret.Host = request.RequestUri;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                return null;
            }
            return ret;
        }

        static HttpWebRequest createRequest(string url)
        {
            HttpWebRequest request = HttpWebRequest.Create(url) as HttpWebRequest;
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";
            request.CookieContainer = new CookieContainer();
            request.AllowAutoRedirect = false; // Do NOT automatically redirect
            request.UserAgent = userAgent;
            return request;
        }

        private string getResponse(string stsUrl, string realm)
        {
            RequestSecurityToken rst = new RequestSecurityToken
            {
                RequestType = WSTrustFeb2005Constants.RequestTypes.Issue,
                AppliesTo = new EndpointAddress(realm),
                KeyType = WSTrustFeb2005Constants.KeyTypes.Bearer,
                TokenType = Microsoft.IdentityModel.Tokens.SecurityTokenTypes.Saml11TokenProfile11
            };

            WSTrustFeb2005RequestSerializer trustSerializer = new WSTrustFeb2005RequestSerializer();

            WSHttpBinding binding = new WSHttpBinding();

            binding.Security.Mode = SecurityMode.TransportWithMessageCredential;

            binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
            binding.Security.Message.EstablishSecurityContext = false;
            binding.Security.Message.NegotiateServiceCredential = false;

            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None;

            EndpointAddress address = new EndpointAddress(stsUrl);

            using (WSTrustFeb2005ContractClient trustClient = new WSTrustFeb2005ContractClient(binding, address))
            {
                trustClient.ClientCredentials.UserName.UserName = _username;
                trustClient.ClientCredentials.UserName.Password = _password;
                Message response = trustClient.EndIssue(
                  trustClient.BeginIssue(
                    Message.CreateMessage(
                      MessageVersion.Default,
                      WSTrustFeb2005Constants.Actions.Issue,
                      new RequestBodyWriter(trustSerializer, rst)
                    ),
                    null,
                    null));
                trustClient.Close();
                using (XmlDictionaryReader reader = response.GetReaderAtBodyContents())
                {
                    return reader.ReadOuterXml();
                }
            }
        }
    }
}

March 11th, 2014 6:30pm

It has been a year since the original question was asked and one of the comments mentioned that Microsoft is aware of the restriction to provide user credentials to download a file. Does anyone know if this has been fixed now? 

I have the access token and but when I try to download the file using CSOM I get a 401.

Free Windows Admin Tool Kit Click here and download it now
April 15th, 2015 2:59am

This topic is archived. No further replies will be accepted.

Other recent topics Other recent topics