OoO: Demo Solution

28 May 2010 , ,    1 Comment

So it took me a little more than the intial couple of days I estimated, but you can finally download the VS2010 Solution for my OoO (aka OpenID, oData and OAuth together) post.

Please don’t take this as gospel or even an example of good practices.

Fire up the website, then fire up the command line client, which will redirect you to the Login page which requires OpenID credentials. There isn’t a fancy OpenID Relaying Party selector, you’ll just have to type it in yourself. That will then let you to generate a verifier token, which lets the CLI client to access the OData service.

This example ignores the scope values being passed in.

Would I use oData?

Yes and no. I’d use oData if I just wanted people to have LINQ access to various resources – but probably resources I wouldn’t try and protect. Why not? While oData lets you define service methods which let you perform more advanced operations and still return the oData format it doesn’t expose these service methods to the WCF Proxy Client (ie, what is generated from Add Service Reference..), but instead requires you to use magic strings. That is, something like:

DataServiceQuery<Product> q = ctx.CreateQuery<Product>("GetProducts")
                                .AddQueryOption("category", "Boats") ;
List<Product> products = q.Execute().ToList();

WCF Data Services lost me on the magic strings bit. I’d probably go for a plain ole WCF Service with SOAP/JSON endpoints if I needed methods instead of just the data.


 

OoO, aka OpenID, oData and OAuth together

I’ve been busy creating a web service or two for MahTweets 3. The biggest problem I have with creating web services is I generally hate storing user credentials/data. Using a delegated identity system like OpenID makes me feel better about it. It also means users of the service don’t have to sign up for yet another service, and can safely use their existing accounts from one of the thousands of OpenID service providers.

However, I wanted to use the service in a desktop client and OpenID is only a browser tech. That’s where OAuth enters the story. OAuth is getting more an more popular for authentication of web services so that users never have to enter in the username/password on unknown websites or desktop clients. Vimeo, Twitter, Yammer, LinkedIn and many more are already using OAuth today.

As for the service itself, of late I’ve been seeing a bit about Astoria/RIA Data Services/WCF Data Services. It’s had a few names in its time. WCF Data Services pushes out OpenData (aka oData), and lets you use LINQ in your client to query the web service. As always, Scott Hanselman has a great post on the basics of OData.

OAuth securing an oData service using OpenID for identity – OoO!

The flow from the client side is request access via OAuth, open browser and login with OpenID, "allow" client via OAuth, copy generated PIN verifier into the client, then have full access to web service. The OpenID portion of it could easily be replaced with username/password, FacebookConnect, or even another OAuth service that can be used for identity like Twitter.

Server Side

The server side stuff is mostly the same as what is found in the DotNetOpenAuth samples. I had problems getting it to work in an MVC Application, but that was solved by just adding SubmitChanges() at the end of ExpireRequestTokenAndStoreNewAccessToken and AuthorizeRequestToken within DatabaseTokenManager.

I’m not going to cover the DotNetOpenAuth setup, its long winded and mostly copy & pasting code.

Create a new WCF Data Service, open up the svc markup (ie, api.svc, not api.svc.cs) and modify the Factory to something like:

<%@ ServiceHost Language="C#"  Factory="MahTweetsWebsite.Application.WCFRestOAuthFactory" Service="MahTweetsWebsite.api" %>

Then create WCFRestOAuthFactory.cs

public class WCFRestOAuthFactory : DataServiceHostFactory
{
    protected override System.ServiceModel.ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
    {
        return new WCFRestOAuth(serviceType, baseAddresses);
    }
}

public class WCFRestOAuth : DataServiceHost
{
    public WCFRestOAuth(Type serviceType, Uri[] baseAddresses) : base(serviceType, baseAddresses)
    {
        this.Authorization.ServiceAuthorizationManager = new OAuthAuthorizationManager();
    }
}

All WCFRestOAuthFactory does is create a new WCF service of the Service specified and assigns the ServiceAuthorizationManager, which in this case is OAuthAuthorizationManager (copied from DNOA). There are other ways you can intercept calls in WCF Data Services to attach security – notably overriding OnStartProcessingRequest(ProcessingRequestArgs args) in the service class itself.

The next dilemma is what if you want one service to provide certain data without authentication? In the MahTweets webservice, things like a list of available Plugins to download doesn’t really need to be authenticated. I’ll admit that I’ve not really sorted this out, but as a basic guess, inspecting the RequestUri inside of OAuthAuthorizationManager is one possibility.

protected override bool CheckAccessCore(OperationContext operationContext)
{
    if (!base.CheckAccessCore(operationContext))
    {
        return false;
    }

    HttpRequestMessageProperty httpDetails = operationContext.RequestContext.RequestMessage.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
    Uri requestUri = operationContext.RequestContext.RequestMessage.Properties["OriginalHttpRequestUri"] as Uri;
    ServiceProvider sp = Constants.CreateServiceProvider();

    if (requestUri.AbsolutePath == "/Api.svc/$metadata")
        return true;

    if (requestUri.AbsolutePath.StartsWith("/Api.svc/Plugins"))
        return true;

// other auth bits

}

I’m not saying this is a great way to do it, but the best I’ve come up with so far.

Client Side

The client side consists of two parts – the OAuth "dance" to get the tokens, much like any OAuth accessing service. Again, I’ll use DotNetOpenAuth for this. This is my console application.

public static class MahTweetsOAuthService
{
    public static readonly ServiceProviderDescription Description = new ServiceProviderDescription
    {
        RequestTokenEndpoint = new MessageReceivingEndpoint("http://localhost:57500/OAuth.ashx", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
        UserAuthorizationEndpoint = new MessageReceivingEndpoint("http://localhost:57500/OAuth.ashx", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
        AccessTokenEndpoint = new MessageReceivingEndpoint("http://localhost:57500/OAuth.ashx", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
        TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
        ProtocolVersion = ProtocolVersion.V10a,
    };
}

public class OAuthWrapper
{
    public DotNetOpenAuth.OAuth.DesktopConsumer consumer { get; set; }

    private String RequestToken = "";
    public String ConsumerKey { get; set; }
    public String ConsumerSecret { get; set; }

    public OAuthWrapper(String ConsumerKey, String ConsumerSecret)
    {
        this.ConsumerKey = ConsumerKey;
        this.ConsumerSecret = ConsumerSecret;

        consumer = new DotNetOpenAuth.OAuth.DesktopConsumer(MahTweetsOAuthService.Description,
            new OAuthTokenManager()
            {
                ConsumerKey = this.ConsumerKey,
                ConsumerSecret = this.ConsumerSecret
            });
    }

    public String BeginAuth()
    {
        var requestArgs = new Dictionary<string, string>();
        requestArgs["scope"] = "http://tempuri.org/IDataApi/GetName|http://tempuri.org/IDataApi/GetAge|http://tempuri.org/IDataApi/GetFavoriteSites";
        return this.consumer.RequestUserAuthorization(requestArgs, null, out this.RequestToken).AbsoluteUri;
    }
    public String CompleteAuth(String Verifier)
    {
        var response = this.consumer.ProcessUserAuthorization(this.RequestToken, Verifier);
        return response.AccessToken;
    }

    public IConsumerTokenManager TokenManager
    {
        get { return (IConsumerTokenManager)consumer.TokenManager; }
    }
}
class Program
{
    static void Main(string[] args)
    {
        //Oauth Authentication
        OAuthWrapper t = new OAuthWrapper("123", "123");
        System.Diagnostics.Process.Start(t.BeginAuth());
        Console.WriteLine("Please enter the PIN");
        t.CompleteAuth(Console.ReadLine());
        Console.WriteLine("Press anykey to continue");
    }
}

The very basic wrapper is designed to make the initial OAuth dance as easy and reusable as possible.

The next part is the WCF Data Services/OData specific bits. OData itself doesn’t specify any standards for authentication, which is a little confusing at first. OAuth requires several headers, one part of that is the OAuth signature which requires the address of the resource you’re requesting to generate.

From what I can see (and I very well may be wrong), there isn’t a really awesome way to do this with WCF Data Services on the client side – if you create an extension on the service (DataServiceContext), it doesn’t yet know the full address (it only has the base address), and if you create an extension for DataServiceQuery<T>, it doesn’t know about the Context so you can’t hijack the SendingRequest event to insert the OAuth header. The best solution I could come up with was creating the extension for DataServiceQuery<T> and passing in the service.

public static class ODataExtensions
{
    public static DataServiceQuery<T> AsOAuth<T>(this DataServiceQuery<T> Query, DataServiceContext context, IConsumerTokenManager t)
    {
        context.SendingRequest += (s, e) =>
        {
            var serviceEndpoint = new MessageReceivingEndpoint(Query.RequestUri.ToString(), HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest);
            var wcf = new DesktopConsumer(MahTweetsOAuthService.Description, t);

            WebRequest httpRequest = wcf.PrepareAuthorizedRequest(serviceEndpoint, ((OAuthTokenManager)t).AccessToken);
            HttpRequestMessageProperty httpDetails = new HttpRequestMessageProperty();
            httpDetails.Headers[HttpRequestHeader.Authorization] = httpRequest.Headers[HttpRequestHeader.Authorization];

            e.Request.Headers = httpDetails.Headers;
        };

        return Query;
    }
}

Usage:

mahtweetsdbEntities api = new ServiceReference2.mahtweetsdbEntities(new Uri("http://localhost:57500/Api.svc/"));
var y = api.Screencasts.AsOAuth(api, t.TokenManager).Execute();

Immediately you’ll see some less than ideal code in there. The default IConsumerTokenManager doesn’t expose the AccessToken, but my OAuthTokenManager does. You could modify the extension to include a string token parameter, or just modify token manager to expose it. The next less-than-generic snippet is the ServiceProviderDescription, which is using the service description from a static class. Again, you could pass in another parameter containing the service description. The last issue is the request method in the service endpoint – this obviously won’t work for other HttpVerbs, so it might be worth passing in that parameter.

It’ll take me a day or three, but I’ll get a sample project together for OoO.