Social Icons

twitter google plus linkedin rss feed

Pages

6.9.12

Authenticating an ASMX Web Service in SharePoint from a WinRT app

Where’s the CookieContainer?

Yes mate, I’ve had that question too.

We are creating a new application for Windows 8 and we need it to connect to our web services. Connecting to the anonymous methods was easy, but when it comes to authenticating the user the situation changes.

I was pretty confident, in this context confident is an euphemism for ignorant and naive,  I could use the same approach I used when I was creating my apps for WP7 I have used it a thousand times and WP7 is more or less the same as WinRT so who would have thought it won’t work?
Well WinRT is not the same as WP7.

After creating the SPAuthBridge class I tried to add the authentication cookie to my SoapClient as usual but as you probably now if you are reading this post this line doesn’t work:

MySoapClient.CookieContainer = SharePointAuth.cookieJar;

In WinRT the SoapClient object doesn’t have a CookieContainer object so we have to find another way of adding the cookie in the calls.

The SPAuthBridge is OK. I have just added a new method to it to get the authentication cookie as string from the cookie container.
public string GetAuthenticationCookie()
{
    return string.Format("{0}={1}", "FedAuth",
            cookieJar.GetCookies(new System.Uri("https://www.mySite.com"))["FedAuth"].Value);
}

Then all you have to do is to follow the steps in the previous post to authenticate and after you have successfully authenticated to SharePoint you have to change the way you add the cookie to your SoapClient.

I have found two ways:

The quick one

Every time you need to call the web service you have to add the cookie to the header and then call the web service in the same OperationContextScope. It looks like this:
using (new OperationContextScope(MyWS.InnerChannel))
{
    HttpRequestMessageProperty httpRequestProperty = new HttpRequestMessageProperty();

    httpRequestProperty.Headers[System.Net.HttpRequestHeader.Cookie] = SharePointAuth.GetAuthenticationCookie();

    OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;

    UserInfo = await MyWS.GetInfoForUserAsync();
}

That’s easy but it’s not very readable and if you have to call a lot of web services you’ll probably get bored of this approach.

The elegant one

Instead of manually adding the cookie every time we can also create a behaviour that does it automatically.

In order to do that you need to create a CookieBehavior class. I copied 99% of mine from here and it looks like this:
class CookieBehaviour: IEndpointBehavior
{
    private string cookie;

    public CookieBehaviour(string cookie)
    {
        this.cookie = cookie;
    }

    public void AddBindingParameters(ServiceEndpoint serviceEndpoint,
        BindingParameterCollection bindingParameters) { }

    public void ApplyClientBehavior(ServiceEndpoint serviceEndpoint, ClientRuntime behavior)
    {
        behavior.ClientMessageInspectors.Add(new CookieMessageInspector(cookie));
    }

    public void ApplyDispatchBehavior(ServiceEndpoint serviceEndpoint,
        EndpointDispatcher endpointDispatcher) { }

    public void Validate(ServiceEndpoint serviceEndpoint) { }
}

public class CookieMessageInspector : IClientMessageInspector
{
    private string cookie;

    public CookieMessageInspector(string cookie)
    {
        this.cookie = cookie;
    }

    public void AfterReceiveReply(ref Message reply,
        object correlationState) { }

    public object BeforeSendRequest(ref Message request, IClientChannel channel)
    {
        HttpRequestMessageProperty httpRequestMessage;
        object httpRequestMessageObject;
        if (request.Properties.TryGetValue(HttpRequestMessageProperty.Name
            , out httpRequestMessageObject))
        {
            httpRequestMessage = httpRequestMessageObject as HttpRequestMessageProperty;
        }
        else
            httpRequestMessage = new HttpRequestMessageProperty();

        httpRequestMessage.Headers[System.Net.HttpRequestHeader.Cookie] = cookie;
        request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessage);

        return null;
    }
}

Once you have this class the only thing you have to do is to add this behaviour to your web service on the initialization and your cookie will be added automatically every time.
public MyWSSoapClient InitializeMyWS()
{
    BasicHttpBinding Bind = new BasicHttpBinding();

    //////////////////////Transport///////////////////////
    Bind.Security.Mode = BasicHttpSecurityMode.Transport;
    //////////////////////////////////////////////////////

    /////////////////////MessageSize//////////////////////
    Bind.MaxReceivedMessageSize = Int32.MaxValue;
    //////////////////////////////////////////////////////

    EndpointAddress oAddress = new EndpointAddress(SiteUrl + "/_vti_bin/MyWS.asmx");
    MyWSSoapClient MyWS = new MyWSSoapClient(Bind, oAddress);

    CookieBehaviour AuthCookieBehaviour = new CookieBehaviour(SharePointAuth.GetAuthenticationCookie());
    MyWS.Endpoint.EndpointBehaviors.Add(AuthCookieBehaviour);

    return MyWS;
}

After you have initialized the web service like this you can call your methods without worrying about the authentication any more:
private async Task<ObservableCollection<Info>> GetInfo()
{
    GetInfoForUserResponse Information = await MyWS.GetInfoForUserAsync();

    return Information.Body.GetInfoForUserResult;
}
The transport is necessary in my case because I am calling a web service hosted in an HTTPS web application by default is set to None and I was getting an ArgumentException like this:

The provided URI scheme 'https' is invalid; expected 'http'.

And the MaxReceivedMessageSize could also be a wee bit excessive. Tune it to suit your needs but be aware that if you set it too small you’ll get a nice CommunicationException saying that:

The maximum message size quota for incoming messages (65536) has been exceeded. To increase the quota, use the MaxReceivedMessageSize property on the appropriate binding element.

Have fun with the tablet if you have one, I am still waiting for mine.

9 comments:

  1. Unfortunately it doesn't work for me.

    I call the authentication service to login and store in a variable the cookie.
    But when I set the cookie in both the quick and elegant approaches it deny the access to the secured method I have in a service in the same site.

    ReplyDelete
  2. Actually yes it does.
    I was cleaning the cookie from the timestamp and that seemed caused the problem.

    ReplyDelete
  3. Hi Josè,
    I have a problem, when I consume the local service when I run the solution it works but when I deploy my site with the service on IIS I get every time Access Denied error even if I'm logged in and I've set the auth cookie.

    Have you any suggestion?

    ReplyDelete
  4. Have you configured your clientaccesspolicy.xml and/or your crossdomain.xml properly to allow external connections to your web application?

    ReplyDelete
  5. Hi please why this return NULL?
    public string GetAuthenticationCookie()
    {
    return string.Format("{0}={1}", "FedAuth",
    cookieJar.GetCookies(new System.Uri("https://www.mySite.com"))["FedAuth"].Value);
    }

    ReplyDelete
    Replies
    1. Obviuslly i'm using my own URL but no work, thanks

      Delete
    2. What is FedAuth?

      Delete
  6. FedAuth is the name of the authentication cookie. If it's null you are probably not authenticated.

    Look into the cookieJar object to see everything you have in there.

    ReplyDelete