OAuth WRAP is a claims based authentication protocol supported by the AppFabric Access Control (ACS) which is part of Windows Azure.
But most importantly it is REST (and thus OData) friendly too.
The idea is that you authenticate against an ACS server and acquire a Simple Web Token or SWT – which contains signed claims about identity / roles / rights etc – and then embed the SWT in requests to a resource server that trusts the ACS server.
The resource server then looks for and verifies the SWT by checking it is correctly signed, before allowing access based on the claims made in the SWT.
If you want to learn more about OAuth WRAP itself here’s the spec.
Now we know the principles behind OAuth WRAP it’s time to map those into the OData world.
Our goal is simple. We want an OData service that uses OAuth WRAP for authorization and a client to test it end to end.
You might be wondering why this post covers OAuth WRAP and not OAuth 2.0.
OAuth 2.0 essentially combines the best features of OAuth 1.0 and OAuth WRAP.
Unfortunately OAuth 2.0 is not yet a ratified standard, so ACS doesn’t support it yet. On the other hand OAuth 1.0 is cumbersome for RESTful protocols like OData. So that leaves OAuth WRAP.
However once it is ratified OAuth 2.0 will essentially depreciate OAuth WRAP and ACS will rev to support it. When that happens you can expect to see a new post in this Authentication Series.
First we’ll provision an ACS server to act as our identity server.
Next we’ll configure our identity server with appropriate roles, scopes and claim transformation rules etc.
Then we’ll create a HttpModule (see part 5) to intercept all requests to the server, which will crack open the SWT, convert it into an IPrincipal and store it in HttpContext.Current.Request.User. This way it can be accessed later for authorization purposes inside the Data Service.
Then we’ll create a simple OData service using WCF Data Services and protect it with a custom HttpModule.
Finally we’ll write client code to authenticate against the ACS server and acquire a SWT token. We’ll use the techniques you saw in part 3 to send the SWT as part of every request to our OData services.
First you’ll need an Windows Azure account and a running AppFabric namespace.
Once your namespace is running you also have a running ACS server.
To correctly configure the ACS server you’ll need to Install the Windows Azure Platform AppFabric SDK which you can find here.
ACM.exe is a command line tool that ships as part of the AppFabric SDK, and that allows you to create Issuers, TokenPolicies, Scopes and Rules.
For an introduction to ACM.exe and ACS look no further than this excellent guide by Keith Brown.
To simplify our acm commands you should edit your ACM.exe.config file to include information about your ACS like this:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="host" value="accesscontrol.windows.net"/> <add key="service" value="{Your service namespace goes here}"/> <add key="mgmtkey" value="{Your Windows Azure Management Key goes here}"/> </appSettings> </configuration>
Doing this saves you from having to re-enter this information every time you run ACM.
Very handy.
Before we start configuring our ACS we need to know a few principles…
Generally claims authentication is used to translate a set of input claims into a signed set of output claims. Sometimes this extends to Federation, which allows trust relationships to be established between identity providers, such that a user on one system can gain access to resources on another system.
However in this blog post we are going to keep it simple and skip federation. Don’t worry though we’ll add federation in the next post.
In ACS terms an Issuer represents a security principal. And whether we want federation or not our first step is to create a new issuer like this:
> acm create issuer -name: partner -issuername: partner -autogeneratekey
This will generate a key which you can retrieve by issuing this command:
> acm getall issuer Count: 1
id: iss_89f12a7ed023c3b7b0a85f32dff96fed2014ad0a name: odata-issuer issuername: odata-issuer key: 9QKoZgtxxU4ABv8uiuvaR+k0cOmUxfEOE0qfPK2lCJY= previouskey: 9QKoZgtxxU4ABv8uiuvaR+k0cOmUxfEOE0qfPK2lCJY= algorithm: Symmetric256BitKey
Our clients are going to need to know this key, so make a note of it for later.
Next we need a token policy. Token Policies specify a timeout indicating how long a new Simple Web Token (or SWT) should be valid, or put another way, how long before the SWT expires.
When creating a token policy you need to balance security versus ease of use and convenience. The shorter the timeout the more likely it is to be based on up to date Identity and Role information, but that comes at the cost of frequent refreshes, which have performance and convenience implications.
For our purposes a timeout of 1 hour is probably about right. So we create a new policy like this:
> acm create tokenpolicy -name: odata-service-policy -timeout: 3600 -autogeneratekey
Where 3600 is the number of seconds in an hour. To see what you created issue this command:
> acm getall tokenpolicy Count: 1
id: tp_aaf3fd9ca64d4471a5c7b5c572c087fb name: odata-service-policy timeout: 3600 key: WRwJkQ9PgbhnIUgKuuovw/6yVAo/Dh0qrb7rqQWnsBk=
We’ll need both the id and key later.
This key is what we share with our resource servers, so that they can check SWTs are correctly signed. We’ll come back to that later.
A service may have multiple ‘scopes’ each with a different set of access rules and rights.
Scopes are linked to a token policy, telling ACS how long SWTs should remain valid, how to sign the SWT, and scopes contain a set of rules which tell ACS how to translate incoming claims into claims embedded in the SWT.
When requesting a SWT a client must include an ‘applies_to’ parameter, which tells ACS for which scope they need a SWT, and consequently which token policy and rules should apply when constructing the SWT.
Here are just some of the reasons you might need multiple scopes:
But for our purposes one scope is enough.
> acm create scope -name: odata-service-scope -appliesto:http://odata.mydomain.com -tokenpolicyid:tp_aaf3fd9ca64d4471a5c7b5c572c087fb
For ‘appliesto’ I chose the url for our planned OData service. Notice too that we bind the scope to the token policy we just created via it’s id.
You can retrieve this scope by executing this:
> acm getall scope Count: 1
id: scp_c028015be790fb5d3ead59307bb3e537d586eac0 name: odata-service appliesto: http://odata.mydomain.com tokenpolicyid: tp_d8c65f770fb14a90bc707e958a722df9
You’ll need to know the scopeid to add Rules to the scope.
ACS has one real job, which you could sum up with these four words: “Claims in, claims out”. Essentially ACS is just a claims transformation engine, and the transformation is achieved by applying a series of rules.
The rules are associated with a scope, and tell ACS how to transform input claims for the target scope (via applies_to) into signed output claims.
In our simple example, all we really want to do is this: ‘If you know the key of my issuer, we’ll sign a claim that you are a ‘User’.
To do that we need this rule:
> acm create rule -name:partner-is-user -scopeid:scp_c028015be790fb5d3ead59307bb3e537d586eac0 -inclaimissuerid:iss_89f12a7ed023c3b7b0a85f32dff96fed2014ad0a -inclaimtype:Issuer -inclaimvalue:partner -outclaimtype:Roles -outclaimvalue:User "Issuer" is a special type of input claim type (normally input claim type is just a string that needs to be found in an incoming SWT) that says anyone who demonstrates direct knowledge of the issuer key will receive a SWT that includes that output claim specified in the rule*.
So this particular rule means anyone who issues an OAuth WRAP request with the Issuer name as the wrap_name and the Issuer key as the wrap_password will receive a signed SWT that claims their "Roles=User".
*NOTE: there are other ways that this rule particular can match, but they are outside the scope of this blog post, check out this excellent guide by Keith Brown for more.
To test that our rule is working try this:
WebClient client = new WebClient(); client.BaseAddress = "https://{your-namespace-goes-here}.accesscontrol.windows.net"; NameValueCollection values = new NameValueCollection(); values.Add("wrap_name", "partner"); values.Add("wrap_password", "9QKoZgtxxU4ABv8uiuvaR+k0cOmUxfEOE0qfPK2lCJY="); values.Add("wrap_scope", "http://odata.mydomain.com"); byte[] responseBytes = client.UploadValues("WRAPv0.9", "POST", values); string response = Encoding.UTF8.GetString(responseBytes); string token = response.Split('&') .Single(value => value.StartsWith("wrap_access_token=")) .Split('=')[1];
Console.WriteLine(token);
When I run that code get this:
Roles%3dUser%26Issuer%3dhttps%253a%252f%252ffabrikamjets.accesscontrol.windows.net%252f%26Audience%3dhttp%253a%252f%252fodata.mydomain.com%26ExpiresOn%3d1282071821%26HMACSHA256%3d%252bc2ZiBpm74Etw%252bAkXY1jNwme8acHfIYd9AAtGMckoss%253d
As you can see the Roles%#dUser is simply a UrlEncoded version of Roles=User, so assuming this is a correctly signed SWT (more on that in Step 3) our rule appears to be working.
Now we have our ACS server correctly configured the next step is to create a HttpModule to crack open SWTs and map them into principles for use inside Data services.
Lets just take the code we wrote in parts 4 & 5 and rework it for OAuth WRAP, firstly by creating a OAuthWrapHttpModule that looks like this:
public class OAuthWrapAuthenticationModule : IHttpModule { public void Init(HttpApplication context) { context.AuthenticateRequest += new EventHandler(context_AuthenticateRequest); } void context_AuthenticateRequest(object sender, EventArgs e) { HttpApplication application = (HttpApplication)sender; if (!OAuthWrapAuthenticationProvider.Authenticate(application.Context)) { Unauthenticated(application); }
} void Unauthenticated(HttpApplication application) { // you could ignore this and rely on authorization logic to // intercept requests etc. But in this example we fail early. application.Context.Response.Status = "401 Unauthorized"; application.Context.Response.StatusCode = 401; application.Context.Response.AddHeader("WWW-Authenticate", "WRAP"); application.CompleteRequest(); } public void Dispose() { } }
As you can see this relies on an OAuthWrapAuthenticationProvider which looks like this:
public class OAuthWrapAuthenticationProvider { static TokenValidator _validator = CreateValidator(); static TokenValidator CreateValidator() { string acsHostname = ConfigurationManager.AppSettings["acsHostname"]; string serviceNamespace = ConfigurationManager.AppSettings["serviceNamespace"]; string trustedAudience = ConfigurationManager.AppSettings["trustedAudience"]; string trustedSigningKey = ConfigurationManager.AppSettings["trustedSigningKey"]; return new TokenValidator( acsHostname, serviceNamespace, trustedAudience, trustedSigningKey ); } public static TokenValidator Validator { get { return _validator; } }
public static bool Authenticate(HttpContext context) { if (!HttpContext.Current.Request.IsSecureConnection) return false;
if (!HttpContext.Current.Request.Headers.AllKeys.Contains("Authorization")) return false;
string authHeader = HttpContext.Current.Request.Headers["Authorization"];
// check that it starts with 'WRAP' if (!authHeader.StartsWith("WRAP ")) { return false; } // the header should be in the form 'WRAP access_token="{token}"' // so lets get the {token} string[] nameValuePair = authHeader .Substring("WRAP ".Length) .Split(new char[] { '=' }, 2);
if (nameValuePair.Length != 2 || nameValuePair[0] != "access_token" || !nameValuePair[1].StartsWith("\"") || !nameValuePair[1].EndsWith("\"")) { return false; }
// trim off the leading and trailing double-quotes string token = nameValuePair[1].Substring(1, nameValuePair[1].Length - 2);
if (!Validator.Validate(token)) return false;
var roles = GetRoles(Validator.GetNameValues(token));
HttpContext.Current.User = new GenericPrincipal( new GenericIdentity("partner"), roles ); return true; } static string[] GetRoles(Dictionary<string, string> nameValues) { if (!nameValues.ContainsKey("Roles")) return new string[] { }; else return nameValues["Roles"].Split(','); } }
As you can see the Authenticate method does a number of things:
In our example the identity itself is hard coded because currently our ACS rules don’t make any claims about the username, it just has role claims. Clearly though if we added more ACS rules you could include a username claim too.
The TokenValidator used in the code above is lifted from Windows Azure AppFabric v1.0 C# samples, which you can find here. If you download and unzip these samples you’ll find the TokenValidator here:
~\AccessControl\GettingStarted\ASPNETStringReverser\CS35\Service\App_Code\TokenValidator.cs
Our create CreateValidator() method creates a shared instance of the TokenValidator, and as you can see we are pulling these settings from web.config:
<configuration> … <appSettings> <add key="acsHostName" value="accesscontrol.windows.net"/> <add key="serviceNamespace" value="{your namespace goes here}"/> <add key="trustedAudience" value="http://odata.mydomain.com"/> <add key="trustedSigningKey" value="{your token policy key goes here}> </appSettings> … </configuration>
The most interesting one is the trustedSigningKey. This is a key shared between ACS and the resource server (in our case our HttpModule). It is the key from the token policy we created in step 2.
The ACS server uses the token policy key to create a hash of the claims (or HMACSHA256) which gets appended to the claims to complete the SWT. Then to verify that the SWT and its claims are valid the resource server simply re-computes the hash and compares.
Now that we’ve got our module we simply need to register it with IIS via the web.config like this:
<configuration> … <system.webServer> <modules> <add name="OAuthWrapAuthenticationModule" type="SimpleService.OAuthWrapAuthenticationModule"/> </modules> </system.webServer> … </configuration>
Next we need to add (if you haven’t already) an OData Service.
There are lots of ways to create an OData Service using WCF Data Services. But by far the easiest way to create a read/write service is using the Entity Framework like this.
Now because we’ve converted the OAuth WRAP SWT into a GenericPrincipal by the time requests hit our Data Service all the authorization techniques we already know using QueryInterceptors and ChangeIntercepts are still applicable.
So you could easily write code like this:
[QueryInterceptor("Orders")] public Expression<Func<Order, bool>> OrdersFilter() { if (!HttpContext.Current.Request.IsAuthenticated) return (Order o) => false; var user = HttpContext.Current.User; if (user.IsInRole("User")) return (Order o) => true; else return (Order o) => false; }
And of course you can rework the HttpModule and interceptors as needed if your claims get more involved.
The final step is to write a client that will send a valid SWT with each OData request.
In part 3 we explored the available client-side hooks. So we know that we can hook up to the DataServiceContext.SendingRequest like this:
ctx.SendingRequest +=new EventHandler<SendingRequestEventArgs>(OnSendingRequest);
And in our event hander we can add headers to the outgoing request. For OAuth WRAP we need to add a authorization header in the form:
Authorization:WRAP access_token="{YOUR SWT GOES HERE}"
NOTE: the double quotes (") are actually part of the format, but the curly bracked ({) are not. See the string.Format call below if you have any doubts.
So our OnSendingRequest event handler looks like this:
static void OnSendingRequest(object sender, SendingRequestEventArgs e) { e.RequestHeaders.Add( "Authorization", string.Format("WRAP access_token=\"{0}\"", GetToken()) ); }
As you can see this uses GetToken() to acquire the actual SWT:
static string GetToken() { if (_token == null){ WebClient client = new WebClient(); client.BaseAddress = "https://{your-namespace-goes-here}.accesscontrol.windows.net"; NameValueCollection values = new NameValueCollection(); values.Add("wrap_name", "partner"); values.Add("wrap_password", "{Issuer Key goes here}"); values.Add("wrap_scope", "http://odata.mydomain.com"); byte[] responseBytes = client.UploadValues("WRAPv0.9", "POST", values); string response = Encoding.UTF8.GetString(responseBytes); string token = response.Split('&') .Single(value => value.StartsWith("wrap_access_token=")) .Split('=')[1];
_token = HttpUtility.UrlDecode(token); } return _token; } static string _token = null;
As you can see we acquire the SWT once (by demonstrating knowledge of the Issuer key)and assuming that is successful we cache it for later reuse.
Finally if we issue queries like say this:
try { foreach (Order order in ctx.Orders) Console.WriteLine(order.Number); } catch (DataServiceQueryException ex) { //var scheme = ex.Response.Headers["WWW-Authenticate"]; var code = ex.Response.StatusCode; if (code == 401) _token = null; }
And our token has expired, as it will after 60 minutes, an exception will occur and we can just null out the cached SWT and any retries will force our code to acquire a new SWT.
In this post we’ve come a long way. We’ve now got a simple OData and OAuth WRAP authentication scenario working end to end.
It is a good foundation to build upon. But there are a few things we can do to make it better.
We could:
We’ll address these issues in Part 9.
Alex James Program Manager Microsoft
The Open Data Protocol (OData) enables you to define data feeds that also make binary large object (BLOB) data, such as photos, videos, and documents, available to client applications that consume OData feeds. These BLOBs are not returned within the feed itself (for obvious serialization, memory consumption and performance reasons). Instead, this binary data, called a media resource (MR), is requested from the data service separately from the entry in the feed to which it belongs, called a media link entry (MLE). An MR cannot exist without a related MLE, and each MLE has a reference to the related MR. (OData inherits this behavior from the AtomPub protocol.) If you are interested in the details and representation of an MLE in an OData feed, see Representing Media Link Entries (either AtomPub or JSON) in the OData Protocol documentation.
To support these behaviors, WCF Data Services defines an IDataServiceStreamProvider interface that, when implemented, is used by the data service runtime to access the Stream that it uses to return or save the MR.
Because it is the most straight-forward way to implement a streaming provider, this initial post in the series demonstrates an IDataServiceStreamProvider implementation that reads binary data from and writes binary data to files stored in the file system as a FileStream. MLE data is stored in a SQL Server database by using the Entity Framework provider. (If you are not already familiar with how to create an OData service by using WCF Data Services, you should first read Getting Started with WCF Data Services and the WCF Data Service quickstart in the MSDN documentation.) Subsequent posts will discuss other strategies and considerations for implementing the IDataServiceStreamProvider interface, such as storing the MR in the database (along with the MLE) and handling concurrency, as well as how to use the WCF Data Services client to consume an MR as a stream in a client application.
This initial blog post will cover the basic requirements for creating a streaming data service, which are:
Now, let’s take a look at the data service that we will use in this blog series.
This blog series features a sample photo data service that implements a streaming provider to store and retrieve image files, along with information about each photo. The following represents the PhotoInfo entity, which is the MLE in this sample data service:
This data is stored in a single PhotoInfo table in a database, which is accessed by using the Entity Framework provider. The following code defines the data service:
// This method is called only once to initialize service-wide policies. public static void InitializeService(DataServiceConfiguration config) { config.SetEntitySetAccessRule("PhotoInfo", EntitySetRights.ReadMultiple | EntitySetRights.ReadSingle | EntitySetRights.AllWrite); config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2; }
You can download the complete source code for this sample data service (and client application) from the MSDN Code Gallery.
The WCF Data Services runtime relies on the IDataServiceStreamProvider implementation to get and set the stream that contains MR data. The methods of IDataServiceStreamProvider that are used to get and set the MR stream are GetReadStream and GetWriteStream, each of which return a Stream.
The GetReadStream method is called by the data service runtime to get the stream that contains the MR that it returns to the requesting client. The entry parameter supplied by the runtime is the MLE, and data from this entity is used to retrieve the related MR data. In our implementation, the image file is retrieved from the app_data directory by using a file name that is based on the key property of the supplied entity, as seen below:
public Stream GetReadStream(object entity, string etag, bool? checkETagForEquality, DataServiceOperationContext operationContext) { if (checkETagForEquality != null) { // This stream provider implementation does not support // ETag headers for media resources. This means that we do not track // concurrency for a media resource, and last-in wins on updates. throw new DataServiceException(400, "This sample service does not support the ETag header for a media resource."); } PhotoInfo image = entity as PhotoInfo; if (image == null) { throw new DataServiceException(500, "Internal Server Error."); } // Build the full path to the stored image file, which includes the entity key. string fullImageFilePath = imageFilePath + "image" + image.PhotoId; if (!File.Exists(fullImageFilePath)) { throw new DataServiceException(500, "The image file could not be found."); } // Return a stream that contains the requested file. return new FileStream(fullImageFilePath, FileMode.Open); }
public Stream GetReadStream(object entity, string etag, bool? checkETagForEquality, DataServiceOperationContext operationContext) { if (checkETagForEquality != null) { // This stream provider implementation does not support // ETag headers for media resources. This means that we do not track // concurrency for a media resource, and last-in wins on updates. throw new DataServiceException(400, "This sample service does not support the ETag header for a media resource."); }
PhotoInfo image = entity as PhotoInfo; if (image == null) { throw new DataServiceException(500, "Internal Server Error."); }
// Build the full path to the stored image file, which includes the entity key. string fullImageFilePath = imageFilePath + "image" + image.PhotoId;
if (!File.Exists(fullImageFilePath)) { throw new DataServiceException(500, "The image file could not be found."); }
// Return a stream that contains the requested file. return new FileStream(fullImageFilePath, FileMode.Open); }
In this implementation, a FileStream is created with read access to the stored image file, and this stream is returned by the data service. In this version of the photo service sample, we are not checking for concurrency in the MR, so we don’t need to worry about the etag or checkETagforEquality parameters. The operationContext parameter provides access to information about the request, including message headers.
As you would expect, the GetWriteStream method is called by the data service to get a stream that is used to store a request from the client that contains an MR. There are two kinds of requests for which the data service uses this implementation:
The GetWriteStream method implementation must support both of these write operations. While the HTTP PUT case turns out to be fairly straightforward, the HTTP POST operation is a bit more complex, for the following reasons:
Because of these complexities, it is helpful to understand the process by which the data service runtime handles a POST request. A new MR/MLE is created by the following process:
POST /PhotoService/PhotoData.svc/PhotoInfo HTTP/1.1 User-Agent: Microsoft ADO.NET Data Services DataServiceVersion: 1.0;NetFx MaxDataServiceVersion: 2.0;NetFx Accept: application/atom+xml,application/xml Accept-Charset: UTF-8 Content-Type: image/jpeg Slug: myphoto.jpg Host: localhost Transfer-Encoding: chunked Expect: 100-continue <<Binary media resource data…>>
POST /PhotoService/PhotoData.svc/PhotoInfo HTTP/1.1 User-Agent: Microsoft ADO.NET Data Services DataServiceVersion: 1.0;NetFx MaxDataServiceVersion: 2.0;NetFx Accept: application/atom+xml,application/xml Accept-Charset: UTF-8 Content-Type: image/jpeg Slug: myphoto.jpg Host: localhost Transfer-Encoding: chunked Expect: 100-continue
<<Binary media resource data…>>
HTTP/1.1 201 Created Cache-Control: no-cache Content-Length: 2048 Content-Type: application/atom+xml;charset=utf-8 Location: http://localhost/PhotoService/PhotoData.svc/PhotoInfo(4) Server: Microsoft-IIS/7.0 X-AspNet-Version: 4.0.30319 DataServiceVersion: 1.0; X-Powered-By: ASP.NET Date: Tue, 27 Jul 2010 06:51:27 GMT <?xml version="1.0" encoding="utf-8" standalone="yes"?> <entry xml:base=http://localhost/PhotoService/PhotoData.svc/ xmlns:d=http://schemas.microsoft.com/ado/2007/08/dataservices xmlns:m=http://schemas.microsoft.com/ado/2007/08/dataservices/metadata xmlns="http://www.w3.org/2005/Atom"> <id>http://localhost/PhotoService/PhotoData.svc/PhotoInfo(4)</id> <title type="text"></title> <updated>2010-07-27T06:51:27Z</updated> <author> <name /> </author> <link rel="edit-media" title="PhotoInfo" href="PhotoInfo(4)/$value" /> <link rel="edit" title="PhotoInfo" href="PhotoInfo(4)" /> <category term="PhotoData.PhotoInfo" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> <content type="image/jpeg" src="PhotoInfo(4)/$value" /> <m:properties xmlns:m=http://schemas.microsoft.com/ado/2007/08/dataservices/metadata xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"> <d:PhotoId m:type="Edm.Int32">4</d:PhotoId> <d:FileName>myphoto.jpg</d:FileName> <d:FileSize m:type="Edm.Int32" m:null="true"></d:FileSize> <d:DateTaken m:type="Edm.DateTime" m:null="true"></d:DateTaken> <d:TakenBy m:null="true"></d:TakenBy> <d:DateAdded m:type="Edm.DateTime">2010-07-26T00:00:00-07:00</d:DateAdded> <d:DateModified m:type="Edm.DateTime">2010-07-26T00:00:00-07:00</d:DateModified> <d:Comments m:null="true"></d:Comments> <d:ContentType>image/jpeg</d:ContentType> <d:Exposure m:type="PhotoData.Exposure"> <d:Aperature m:type="Edm.Decimal" m:null="true"></d:Aperature> <d:ShutterSpeed m:type="Edm.Int16" m:null="true"></d:ShutterSpeed> <d:FilmSpeed m:type="Edm.Int16" m:null="true"></d:FilmSpeed> </d:Exposure> <d:Dimensions m:type="PhotoData.Dimensions"> <d:Height m:type="Edm.Int16" m:null="true"></d:Height> <d:Width m:type="Edm.Int16" m:null="true"></d:Width> </d:Dimensions> </m:properties>
HTTP/1.1 201 Created Cache-Control: no-cache Content-Length: 2048 Content-Type: application/atom+xml;charset=utf-8 Location: http://localhost/PhotoService/PhotoData.svc/PhotoInfo(4) Server: Microsoft-IIS/7.0 X-AspNet-Version: 4.0.30319 DataServiceVersion: 1.0; X-Powered-By: ASP.NET Date: Tue, 27 Jul 2010 06:51:27 GMT
<?xml version="1.0" encoding="utf-8" standalone="yes"?> <entry xml:base=http://localhost/PhotoService/PhotoData.svc/ xmlns:d=http://schemas.microsoft.com/ado/2007/08/dataservices xmlns:m=http://schemas.microsoft.com/ado/2007/08/dataservices/metadata xmlns="http://www.w3.org/2005/Atom"> <id>http://localhost/PhotoService/PhotoData.svc/PhotoInfo(4)</id> <title type="text"></title> <updated>2010-07-27T06:51:27Z</updated> <author> <name /> </author> <link rel="edit-media" title="PhotoInfo" href="PhotoInfo(4)/$value" /> <link rel="edit" title="PhotoInfo" href="PhotoInfo(4)" /> <category term="PhotoData.PhotoInfo" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> <content type="image/jpeg" src="PhotoInfo(4)/$value" /> <m:properties xmlns:m=http://schemas.microsoft.com/ado/2007/08/dataservices/metadata xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"> <d:PhotoId m:type="Edm.Int32">4</d:PhotoId> <d:FileName>myphoto.jpg</d:FileName> <d:FileSize m:type="Edm.Int32" m:null="true"></d:FileSize> <d:DateTaken m:type="Edm.DateTime" m:null="true"></d:DateTaken> <d:TakenBy m:null="true"></d:TakenBy> <d:DateAdded m:type="Edm.DateTime">2010-07-26T00:00:00-07:00</d:DateAdded> <d:DateModified m:type="Edm.DateTime">2010-07-26T00:00:00-07:00</d:DateModified> <d:Comments m:null="true"></d:Comments> <d:ContentType>image/jpeg</d:ContentType> <d:Exposure m:type="PhotoData.Exposure"> <d:Aperature m:type="Edm.Decimal" m:null="true"></d:Aperature> <d:ShutterSpeed m:type="Edm.Int16" m:null="true"></d:ShutterSpeed> <d:FilmSpeed m:type="Edm.Int16" m:null="true"></d:FilmSpeed> </d:Exposure> <d:Dimensions m:type="PhotoData.Dimensions"> <d:Height m:type="Edm.Int16" m:null="true"></d:Height> <d:Width m:type="Edm.Int16" m:null="true"></d:Width> </d:Dimensions> </m:properties>
For a POST request, our implementation of GetWriteStream sets any non-nullable property of PhotoInfo (otherwise the Entity Framework provider raises an error) and returns a new FileStream based on a temp file.
public Stream GetWriteStream(object entity, string etag, bool? checkETagForEquality, DataServiceOperationContext operationContext) { if (checkETagForEquality != null) { // This stream provider implementation does not support // ETags associated with BLOBs. This means that we do not // track concurrency for a media resource and last-in wins on updates. throw new DataServiceException(400, "This demo does not support ETags associated with BLOBs"); } PhotoInfo image = entity as PhotoInfo; if (image == null) { throw new DataServiceException(500, "Internal Server Error: " + "the Media Link Entry could not be determined."); } // Handle the POST request. if (operationContext.RequestMethod == "POST") { // Set the file name from the Slug header; if we don't have a // Slug header, just set a temporary name which is overwritten // by the subsequent MERGE request from the client. image.FileName = operationContext.RequestHeaders["Slug"] ?? "newFile"; // Set the required DateTime values. image.DateModified = DateTime.Today; image.DateAdded = DateTime.Today; // Set the content type, which cannot be null. image.ContentType = operationContext.RequestHeaders["Content-Type"]; // Cache the current entity to enable us to both create a // key-based storage file name and to maintain transactional // integrity in the disposer; we do this only for a POST. cachedEntity = image; return new FileStream(tempFile, FileMode.Open); } // Handle the PUT request else { // Return a stream to write to an existing file. return new FileStream(imageFilePath + "image" + image.PhotoId.ToString(), FileMode.Open, FileAccess.Write); } }
public Stream GetWriteStream(object entity, string etag, bool? checkETagForEquality, DataServiceOperationContext operationContext) { if (checkETagForEquality != null) { // This stream provider implementation does not support // ETags associated with BLOBs. This means that we do not // track concurrency for a media resource and last-in wins on updates. throw new DataServiceException(400, "This demo does not support ETags associated with BLOBs"); }
PhotoInfo image = entity as PhotoInfo;
if (image == null) { throw new DataServiceException(500, "Internal Server Error: " + "the Media Link Entry could not be determined."); }
// Handle the POST request. if (operationContext.RequestMethod == "POST") { // Set the file name from the Slug header; if we don't have a // Slug header, just set a temporary name which is overwritten // by the subsequent MERGE request from the client. image.FileName = operationContext.RequestHeaders["Slug"] ?? "newFile";
// Set the required DateTime values. image.DateModified = DateTime.Today; image.DateAdded = DateTime.Today;
// Set the content type, which cannot be null. image.ContentType = operationContext.RequestHeaders["Content-Type"];
// Cache the current entity to enable us to both create a // key-based storage file name and to maintain transactional // integrity in the disposer; we do this only for a POST. cachedEntity = image;
return new FileStream(tempFile, FileMode.Open); } // Handle the PUT request else { // Return a stream to write to an existing file. return new FileStream(imageFilePath + "image" + image.PhotoId.ToString(), FileMode.Open, FileAccess.Write); } }
Note that the portion of this implementation that handles the PUT request simply returns a FileStream that accesses the image file based on the key value.
After the POST operation succeeds, the temp file created by GetWriteStream is renamed to include the key value and moved to the app_data folder in the disposer, as shown by the following:
public void Dispose() { // If we have a cached entity, it must be a POST request. if (cachedEntity != null) { // Get the new entity from the Entity Framework object state manager. ObjectStateEntry entry = this.context.ObjectStateManager.GetObjectStateEntry(cachedEntity); if (entry.State == System.Data.EntityState.Unchanged) { // Since the entity was created successfully, move the temp file into the // storage directory and rename the file based on the new entity key. File.Move(tempFile, imageFilePath + "image" + cachedEntity.PhotoId.ToString()); // Delete the temp file. File.Delete(tempFile); } else { // A problem must have occurred when saving the entity to the // database, so we should delete the entity and temp file. context.DeleteObject(cachedEntity); File.Delete(tempFile); throw new DataServiceException("An error occurred. " + "The photo could not be saved."); } } }
public void Dispose() { // If we have a cached entity, it must be a POST request. if (cachedEntity != null) {
// Get the new entity from the Entity Framework object state manager.
ObjectStateEntry entry = this.context.ObjectStateManager.GetObjectStateEntry(cachedEntity);
if (entry.State == System.Data.EntityState.Unchanged) { // Since the entity was created successfully, move the temp file into the // storage directory and rename the file based on the new entity key. File.Move(tempFile, imageFilePath + "image" + cachedEntity.PhotoId.ToString());
// Delete the temp file. File.Delete(tempFile); } else { // A problem must have occurred when saving the entity to the // database, so we should delete the entity and temp file. context.DeleteObject(cachedEntity); File.Delete(tempFile);
throw new DataServiceException("An error occurred. " + "The photo could not be saved."); } } }
This also enables us to maintain some level of transactional consistency between creation of the MR and MLE from a POST request when using the Entity Framework provider; the photo service deletes the new image file if the MLE fails to be created.
While GetReadStream and GetWriteStream are the primary methods that we implement, the IDataServiceStreamProvider interface contains several other members that are also required.
public void DeleteStream(object entity, DataServiceOperationContext operationContext) { PhotoInfo image = entity as PhotoInfo; if (image == null) { throw new DataServiceException(500, "Internal Server Error."); } try { // Delete the requested file by using the key value. File.Delete(imageFilePath + "image" + image.PhotoId.ToString()); } catch (IOException ex) { throw new DataServiceException("The image could not be found.", ex); } }
public void DeleteStream(object entity, DataServiceOperationContext operationContext) { PhotoInfo image = entity as PhotoInfo; if (image == null) { throw new DataServiceException(500, "Internal Server Error."); }
try { // Delete the requested file by using the key value. File.Delete(imageFilePath + "image" + image.PhotoId.ToString()); } catch (IOException ex) { throw new DataServiceException("The image could not be found.", ex); } }
public string GetStreamContentType(object entity, DataServiceOperationContext operationContext) { // Get the PhotoInfo entity instance. PhotoInfo image = entity as PhotoInfo; if (image == null) { throw new DataServiceException(500, "Internal Server Error."); } return image.ContentType; }
public string GetStreamContentType(object entity, DataServiceOperationContext operationContext) { // Get the PhotoInfo entity instance. PhotoInfo image = entity as PhotoInfo; if (image == null) { throw new DataServiceException(500, "Internal Server Error."); }
return image.ContentType; }
public string ResolveType(string entitySetName, DataServiceOperationContext operationContext) { // We should only be handling PhotoInfo types. if (entitySetName == "PhotoInfo") { return "PhotoService.PhotoInfo"; } else { // This will raise an DataServiceException. return null; } }
For more information about implementing this interface, see Streaming Provider (WCF Data Services) in the MSDN documentation.
Whenever a custom data service provider (of which IDataServiceStreamProvider is one) is implemented in a WCF Data Service, the IServiceProvider interface should also be implemented. IServiceProvider provides the data service runtime with a specific provider implementation, in our case the IDataServiceStreamProvider implementation that returns a PhotoServiceStreamProvider instance. The following code implements the GetService method required by IServiceProvider :
public object GetService(Type serviceType) { if (serviceType == typeof(IDataServiceStreamProvider)) { // Return the stream provider to the data service. return new PhotoServiceStreamProvider(this.CurrentDataSource); } return null; }
In order for the data service to return MR data as a stream, it has to know for which entity to invoke IDataServiceStreamProvider. An MLE is identified by using the HasStream attribute. When using the reflection provider or a custom data service provider, HasStreamAttribute is applied to the entity type that is the MLE. Because our sample data service uses the Entity Framework provider, we must instead manually apply the HasStream attribute to the PhotoInfo entity in the CSDL portion of the .edmx file that represents our data model, as seen in the following XML fragment:
For more information about using the Entity Framework data provider, see Entity Framework Provider (WCF Data Services) in the MSDN documentation.
The HasStream attribute is also included in the model metadata returned by the service to client applications. When the WCF Data Services client finds the HasStream attribute on the PhotoInfo entity, it also applies HasStreamAttribute to the generated PhotoInfo class on the client.
The WCF Data Services runtime hosts a basic implementation of Windows Communication Foundation (WCF) that it uses for HTTP messaging. Because WCF limits the size of data streams, we must configure the data service endpoint to enable large streams. The following element configures our photo service to receive images up to 500KB:
<system.serviceModel> <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/> <services> <!-- The name of the service --> <service name="PhotoService.PhotoData"> <!-- you can leave the address blank or specify your end point URI --> <endpoint binding="webHttpBinding" bindingConfiguration="higherMessageSize" contract="System.Data.Services.IRequestHandler"> </endpoint> </service> </services> <bindings> <webHttpBinding> <!-- configure the maxReceivedMessageSize value to suit the max size of the request (in bytes) you want the service to recieve--> <binding name="higherMessageSize" maxReceivedMessageSize="500000"/> </webHttpBinding> </bindings> </system.serviceModel>
To accept binary streams larger than 500KB, you must set a larger value for the maxReceivedMessageSize attribute.
The IDataServiceStreamProvider implementation uses a FileStream to access image files from both the temp file directory and the Web site’s app_data folder. We must grant at least modify access to the the process under which the data service runs to access those folders. This same access must be granted in the database itself by creating a new login for this account, and this login must be granted access to the database. The SQL script included in the streaming sample project creates the necessary logins in the server and grants access to the database.
At this point, our photo data service is ready to return image files as a stream. We can access the PhotoInfo feed in a Web browser. Sending a GET request to the URI http://localhost/PhotoService/PhotoData.svc/PhotoInfo returns the following feed (with feed-reading disabled in the browser):
Note that the PhotoInfo(1) entry has a Content element of type image/jpeg and an edit-media link, both of which reference the relative URI of the media resource (PhotoInfo(1)/$value).
When we browse to this URI, the data service returns the related MR file as a stream, which is displayed as a JPEG image in the Web browser, as follows:
In the next post in the series, we will show how to use the WCF Data Services client to not only access media resources as streams, but to also create and update image files by generating POST and PUT requests against this same data service.
Glenn GaileySenior Programming WriterWCF Data Services
Our goal in this post is to re-use the Forms Authentication already in a website to secure a new Data Service.
To bootstrap this we need a website that uses Forms Auth.
Turns out the MVC Music Store Sample is perfect for our purposes because:
The rest of this post assumes you’ve downloaded and installed the MVC Music Store sample.
The MVC Music Store sample already has Forms Authentication enabled in the web.config like this:
<authentication mode="Forms"> <forms loginUrl="~/Account/LogOn" timeout="2880" /> </authentication>
With this in place any services we add to this application will also be protected.
If you double click the StoreDB.edmx file inside the Models folder you’ll see something like this:
This is want we want to expose, so the first step is to click ‘Add New Item’ and then select new WCF Data Service:
Next modify your MusicStoreService to look like this:
public class MusicStoreService : DataService<MusicStoreEntities> { // This method is called only once to initialize service-wide policies. public static void InitializeService(DataServiceConfiguration config) { config.SetEntitySetAccessRule("Carts", EntitySetRights.None); config.SetEntitySetAccessRule("OrderDetails", EntitySetRights.ReadSingle); config.SetEntitySetAccessRule("*", EntitySetRights.AllRead); config.SetEntitySetPageSize("*", 50); config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2; } }
The PageSize is there to enforce Server Driven Paging, which is an OData best practice, we don’t like to show samples that skip this… :)
Then the three EntitySetAccessRules in turn:
Next we need to secure our ‘sensitive data’, which means making sure only appropriate people can see Orders and OrderDetails, by adding two QueryInterceptors to our MusicStoreService:
[QueryInterceptor("Orders")] public Expression<Func<Order, bool>> OrdersFilter() { if (!HttpContext.Current.Request.IsAuthenticated) return (Order o) => false;
var username = HttpContext.Current.User.Identity.Name; if (username == "Administrator") return (Order o) => true; else return (Order o) => o.Username == username; }
[QueryInterceptor("OrderDetails")] public Expression<Func<OrderDetail, bool>> OrdersFilter() { if (!HttpContext.Current.Request.IsAuthenticated) return (OrderDetail od) => false;
var username = HttpContext.Current.User.Identity.Name; if (username == "Administrator") return (OrderDetail od) => true; else return (OrderDetail od) => od.Order.Username == username; }
These interceptors filter out all Orders and OrderDetails if the request is unauthenticated.
They allow the administrator to see all Orders and OrderDetails, but everyone else can only see Orders / OrderDetails that they created.
That’s it - our service is ready to go.
NOTE: if you have a read-write service and you want to authorize updates you need ChangeInterceptors.
The easiest way to logon is to add something to your cart and buy it:
Which prompts you to logon or register:
The first time through you’ll need to register, which will also log you on, and then once you are logged on you’ll need to retry checking out.
This has the added advantage of testing our security. Because at the end of the checkout process you will be logged in as the user you just registered, meaning if you browse to your Data Service’s Orders feed you should see the order you just created:
If however you logoff, or restart the browser, and try again you’ll see an empty feed like this:
Perfect. Our query interceptors are working as intended.
This all works because Forms Authentication is essentially just a HttpModule, which sits under our Data Service, that relies on the browser (or client) passing around a cookie once it has logged on.
By the time the request gets to the DataService the HttpContext.Current.Request.User is set.
Which in turn means our query interceptors can enforce our custom Authorization logic.
In authentication terms a browser is a passive client, that’s because basically it does what it is told, a server can redirect it to a logon page which can redirect it back again if successful, it can tell it to include a cookie in each request and so on...
Often however it is active clients – things like custom applications and generic data browsers – that want to access the OData Service.
How do they authenticate?
They could mimic the browser, by responding to redirects and programmatically posting the logon form to acquire the cookie. But no wants to re-implement html form handling just to logon.
Thankfully there is a much easier way.
You can enable an standard authentication endpoint, by adding this to your web.config:
<system.web.extensions> <scripting> <webServices> <authenticationService enabled="true" requireSSL="false"/> </webServices> </scripting> </system.web.extensions>
The endpoint (Authentication_JSON_AppService.axd) makes it much easier to logon programmatically.
Now that we’ve enabled the authentication endpoint, lets see how we use it. Essentially for forms authentication to work the DataServiceContext must include a valid cookie with every request.
A cookie is just a http header and, as we saw in part 3, it is very easy to add a custom header with every request.
But before we get down to setting cookies, in some scenarios there is an even easy way: using Client Application Services. These services are not available in the .NET Client Profile (or Silverlight) so you may need to change your Target Framework to use them:
Once you’ve done that you enable Client Application Services like this:
NOTE: the Authentication Services Location should be set to the root of the website that has Authentication Services enabled.
Next you add a reference to System.Web to gain access to System.Web.Security.Membership.
Once you’ve done this you simply need to logon once:
System.Web.Security.Membership.ValidateUser("Alex", "password");
This logs on and stores the resulting cookie on the current thread.
Next, assuming you already have a Service Reference to your Data Service – see this to learn how – you can extend your custom DataServiceContext, in our example called MusicStoreEntities, to automatically send the cookie with each request:
public partial class MusicStoreEntities { partial void OnContextCreated() { this.SendingRequest += new EventHandler<SendingRequestEventArgs>(OnSendingRequest); } void OnSendingRequest(object sender, SendingRequestEventArgs e) { ((HttpWebRequest)e.Request).CookieContainer = ((ClientFormsIdentity)Thread.CurrentPrincipal.Identity).AuthenticationCookies; } }
This works by adding the partial OnContextCreated method, which is called in the MusicStoreEntities constructor, and hooking up to the SendingRequest event, to set the cookie for each request.
That’s it, pretty easy.
If however using Client Application Services is not an option – for example you’re in Silverlight or you can only use the Client Profile – you will have to manually get and set the cookie.
To do this change the example above to look like this instead:
public partial class MusicStoreEntities { partial void OnContextCreated() { this.SendingRequest += new EventHandler<SendingRequestEventArgs>(OnSendingRequest); } public void OnSendingRequest(object sender, SendingRequestEventArgs e) { e.RequestHeaders.Add("Cookie", GetCookie("Alex", "password")); } string _cookie; string GetCookie(string userName, string password) { if (_cookie == null) { string loginUri = string.Format("{0}/{1}/{2}", "http://localhost:1397", "Authentication_JSON_AppService.axd", "Login"); WebRequest request = HttpWebRequest.Create(loginUri); request.Method = "POST"; request.ContentType = "application/json";
string authBody = String.Format( "{{ \"userName\": \"{0}\", \"password\": \"{1}\", \"createPersistentCookie\":false}}", userName, password); request.ContentLength = authBody.Length;
StreamWriter w = new StreamWriter(request.GetRequestStream()); w.Write(authBody); w.Close();
WebResponse res = request.GetResponse(); if (res.Headers["Set-Cookie"] != null) { _cookie = res.Headers["Set-Cookie"]; } else { throw new SecurityException("Invalid username and password"); } } return _cookie; } }
This code is admittedly a little more involved. But it you break it down it all makes sense.
The code adds the cookie to the headers whenever a request is issued.
The hardest part is actually acquiring the cookie. The GetCookie() method checks whether we have a cookie, if not it creates a request to the Authentication endpoint, passing the username and password in a JSON body.
If authentication is successful the response will include a ‘Set-Cookie’ header, that contains the cookie.
We’ve just walked through using Forms Authentication with an OData service.
That included: integrating security with an existing website, enabling both browser and active clients – based on DataServiceContext – and authenticating from any .NET client.
Next up we’ll start looking at things like OAuth and OAuthWrap…
Alex James Program Manager Microsoft.
You might remember, from Part 5, that Basic Authentication is built-in to IIS.
So why do we need ‘Custom’ Basic Authentication?
Well if you are happy using windows users and passwords you don’t.
That’s because the built-in Basic Authentication, uses the Basic Authentication protocol, to authenticate against the windows user database.
If however you have a custom user/password database, perhaps it’s part of your application database, then you need ‘Custom’ Basic Authentication.
Basic authentication is a very simple authentication scheme, that should only be used in conjunction with SSL or in scenarios where security isn’t paramount.
If you look at how a basic authentication header is fabricated, you can see why it is NOT secure by itself:
var creds = "user" + ":" + "password"; var bcreds = Encoding.ASCII.GetBytes(creds); var base64Creds = Convert.ToBase64String(bcreds); authorizationHeader = "Basic " + base64Creds;
Yes that’s right the username and password are Base64 encoded and shipped on the wire for the whole world to see, unless of course you are also using SSL for transport level security.
Nevertheless many systems use basic authentication. So it’s worth adding to your arsenal.
Creating a Custom Basic Authentication module should be no harder than cracking Basic Auth, i.e. it should be child’s play.
We can use our HttpModule from Part 5 as a starting point:
public class BasicAuthenticationModule: IHttpModule { public void Init(HttpApplication context) { context.AuthenticateRequest += new EventHandler(context_AuthenticateRequest); } void context_AuthenticateRequest(object sender, EventArgs e) { HttpApplication application = (HttpApplication)sender; if (!BasicAuthenticationProvider.Authenticate(application.Context)) { application.Context.Response.Status = "401 Unauthorized"; application.Context.Response.StatusCode = 401; application.Context.Response.AddHeader("WWW-Authenticate", "Basic"); application.CompleteRequest(); } } public void Dispose() { } }
The only differences from Part 5 are:
The final step is vital because without this clients that don’t send credentials by default – like HttpWebRequest and by extension DataServiceContext – won’t know to retry with the credentials when their first attempt fails.
The Authenticate method is unchanged from our example in Part 5:
IPrincipal principal; if (TryGetPrincipal(authHeader, out principal)) { HttpContext.Current.User = principal; return true; } return false; }
Our new TryGetPrincipal method looks like this:
private static bool TryGetPrincipal(string authHeader, out IPrincipal principal) { var creds = ParseAuthHeader(authHeader); if (creds != null && TryGetPrincipal(creds, out principal)) return true;
principal = null; return false; }
As you can see it uses ParseAuthHeader to extract the credentials from the authHeader – so they can be checked against our custom user database in the other TryGetPrincipal overload:
private static string[] ParseAuthHeader(string authHeader) { // Check this is a Basic Auth header if ( authHeader == null || authHeader.Length == 0 || !authHeader.StartsWith("Basic") ) return null;
// Pull out the Credentials with are seperated by ':' and Base64 encoded string base64Credentials = authHeader.Substring(6); string[] credentials = Encoding.ASCII.GetString( Convert.FromBase64String(base64Credentials) ).Split(new char[] { ':' }); if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0]) || string.IsNullOrEmpty(credentials[0]) ) return null;
// Okay this is the credentials return credentials; }
First this code checks that this is indeed a Basic auth header and then attempts to extract the Base64 encoded credentials from the header.
If everything goes according to plan the array returned will have two elements: the username and the password.
Next we check our ‘custom’ user database to see if those credentials are valid.
In this toy example I have it completely hard coded:
private static bool TryGetPrincipal(string[] creds,out IPrincipal principal) { if (creds[0] == "Administrator" && creds[1] == "SecurePassword") { principal = new GenericPrincipal( new GenericIdentity("Administrator"), new string[] {"Administrator", "User"} ); return true; } else if (creds[0] == "JoeBlogs" && creds[1] == "Password") { principal = new GenericPrincipal( new GenericIdentity("JoeBlogs"), new string[] {"User"} ); return true; } else { principal = null; return false; } }
You’d probably want to check a database somewhere, but as you can see that should be pretty easy, all you need is a replace this method with whatever code you want.
Finally you just need to do is add this to your WebConfig:
<system.webServer> <modules> <add name="BasicAuthenticationModule" type="SimpleService.BasicAuthenticationModule"/> </modules> </system.webServer>
If you want to allow some unauthenticated access to your Data Service, you could change your BasicAuthenticationModule so it doesn’t ‘401’ if the Authenticate() returns false.
Then if certain queries or updates actually require authentication or authentication, you could check HttpContext.Current.Request.IsAuthenticated or HttpContext.Current.Request.User in QueryInterceptors and ChangeInterceptors as necessary.
This approach allows you to mix and match your level of security.
See part 4 for more on QueryInterceptors.
When you try to connect to an OData service protected with Basic Authentication (Custom or built-in) you have two options:
You can use a Credentials Cache like this.
var serviceCreds = new NetworkCredential("Administrator", "SecurePassword"); var cache = new CredentialCache(); var serviceUri = new Uri("http://localhost/SimpleService"); cache.Add(serviceUri, "Basic", serviceCreds); ctx.Credentials = cache;
When you do this the first time Data Services attempts to connect to the Service the credentials aren’t sent – so a 401 is received.
However so long as the service challenges using the "WWW-Authenticate" response header, it will seamlessly retry under the hood.
Another option is to just create and send the authentication header yourself.
1) Hook up to the DataServiceContext’s SendingRequest Event:
2) Add the Basic Authentication Header to the request:
static void OnSendingRequest(object sender, SendingRequestEventArgs e) { var creds = "user" + ":" + "password"; var bcreds = Encoding.ASCII.GetBytes(creds); var base64Creds = Convert.ToBase64String(bcreds); e.RequestHeader.Add("Authorization", "Basic " + base64Creds); }
As you can see this is pretty simple. And has the advantage that it will work even if the server doesn’t respond with a challenge (i.e. WWW-Authenticate header).
You now know how to implement Basic Authentication over a custom credentials database and how to interact with a Basic Authentication protected service using the Data Service Client.
Next up we’ll look at Forms Authentication in depth.
Windows Azure and SQL Azure are the new Cloud service products from Microsoft. In this blog post, I am going to show you how you can take a database that is hosted in SQL Azure and expose it as OData in a rich way using WCF Data Services and Windows Azure.
This walk-through requires that you have Visual Studio 2010 and both a Windows Azure and SQL Azure account.
Step 1: Configure the Database in SQL Azure
SQL Azure provides a great way to host your database in the cloud. I won’t spend a lot of time explaining how to use SQL Azure as there are a number of other blogs that have covered this in great detail. I have used the SQL Azure developer portal along with SQL Server Management Studio to create a Northwind database on my SQL Azure account that I will later expose VIA OData. The key to this step is that you want to have created your database in SQL Azure and you need to click the “Allow Microsoft Services access to this server” in the firewall settings on the SQL Azure developer portal as shown in the screen capture below – this allows your service in Windows Azure to access your database.
When creating your SQL Azure server, carefully consider the server location you choose. In my case I have selected the North Central US location for my SQL Azure server and the key is that when you later choose a Windows Azure server location to deploy your Data Service to, you will probably want to choose the same location to reduce the latency between the Data Service and the database.
Step 2: Create the Data Service
First, you will need to go here and download the Windows Azure Tools for Visual Studio to get support for creating Windows Azure projects in Visual Studio 2010. Once you have done that, create a new solution in Visual Studio and choose the new Windows Azure Cloud Service project type.
Next, you will be asked to choose the Roles for your Cloud Service. Since our cloud service will be used primarily to host the Data Service the only role we need to add is the ASP.NET Web Role.
Once you click ok, Visual Studio creates a Windows Azure solution with Azure server configuration files and a single ASP.NET Web Role. Included in you project will also be an ASP.NET project that you can program against like you would any other ASP.NET project that is not being hosted in Windows Azure. The next thing you will do is create a WCF Data Service that exposes the Northwind Database you created in SQL Azure in step 1. To do this, create an Entity Framework model over that SQL Azure database using the Add New Item –> ADO.NET Entity Data Model gesture in Visual Studio.
In the next screen, choose to generate your model from a database and then specify the database to connect to. Make sure when you setup your SQL Azure database that you have chosen to allow the IP Address of your development system access to the SQL Azure database. Define a standard SqlClient connection to your SQL Azure database and specify your username and password to the database. Once you have defined your connection, select OK and then follow the steps through the rest of the Wizard to finish creating the model.
Next, use the Add New Item option to add a WCF Data Service to your ASP.NET project. Set the data source for your data service as the EF model you just created and set the entity set access rights for each entity set in your model.
Right-click the .svc file and select “View In Browser” and you should see the service document in your default browser. At this point, you should spend some more time configuring your data service – you can user query interceptors to add finer grainer authorization, setup authentication, set paging limits, etc. Because this goal of this post is to walk you through deploying the service, I will skip all of that and begin deploying the service immediately.
Step 3: Deploy the Data Service
The begin deploying the service to Windows Azure, right-click on the Web Role project and select “Publish…”. You will be presented with the following screen which walks you through the process of setting up deployment to your Azure server.
To start, setup your credentials (skip this step if you have deployed from Visual Studio 2010 before and have already setup your credentials). From the credentials drop-down, select “Add”. You will be presented with another dialog for creating your credentials file.
From the first drop down, select “<Create>” and then give the credentials a name – I called mine ODataAzure". When you do this, the Visual Studio 2010 tools will create a credentials file and place it in a temporary folder on disk. Click the “Copy the full path” link in the dialog to copy the path of this file to the clipboard. You will need this location for the next step, which is to upload this credentials file to your Azure server.
The next step is to go to the Windows Azure developer portal here and sign in to your Azure Account. Once you are logged in, go to the “Account” tab and click “Manage My API Certificates”. In this screen, there is an option to Upload a certificate – click this option and paste in the location of the certificate you just created and upload the certificate.
Once you have uploaded the certificate, go back to the Summary page and if you have not already created a Storage Account and Hosted Service, do so now. Click the “New Service” button and follow the prompts to create a storage model and a hosted service, remember to use the same service location for these as you chose for your SQL Azure database in step 1. When completed your summary should be similar to the one shown below.
Now go back to the Account page and copy the Subscription ID from the bottom of the page under the “Support Information” label. Go back to Visual Studio 2010 and paste that subscription ID into the certificate dialog. Name your credentials and click OK. At this point you have completed configuring Visual Studio 2010 to publish to your Azure Account. You will not need to perform these steps again in the future because Visual Studio remembers your credentials information and knows how to publish to your Azure account.
Now you will be back in the publish cloud service dialog, if you select the credentials you just created from the Credentials drop down, the Hosted Service and Storage Account drop downs will be populated with the services you just created. Adjust the label for the deployment if you need to and select OK – Visual Studio will start the deployment to Windows Azure. This will only take a few minutes.
Step 4: Move the Service to a Production Server
In Windows Azure, there is two stages to deploying your service. In the first stage, you deploy the service to the staging servers – these are the private servers you use to test and validate your service before you publish it to the public production server. To configure this, open the Windows Azure developer portal again, go to the Summary Tab and select your Hosted Service from the summary screen. This will bring up the summary screen for your hosted service and give you a visual representation of what is running on your production and staging servers. The screen capture below shows my Hosted Service summary and because I have previously published my project to the production server, I have an active service running on the production server (indicated by the blue cube). You will have separate URLs for the staging and production versions of your service. When you are satisfied with the service as it is running on the Staging server, you can click the circle between the staging and production cube to begin the process of deploying your service on the Production server.
When the deployment onto the production server has completed, you will see the green check mark below the Production service (as in the picture above). You now have a completed Data Service running in the cloud the is exposing your SQL Azure database! Click on the Web Site URL to open the ASP.NET web site that hosts your data service (append the .svc document to the end of the URL to get the service document of your service).
This is a fully functioning WCF Data Service that fully supports the OData protocol. Check out the Consumers page of the odata.org website to see a list of all the client applications and client libraries that you can now target your web service with.
Shayne Burgess
Program Manager
WCF Data Services and OData
In the last post we saw how to add custom authentication inside your Data Service using the ProcessingRequest event.
Unfortunately that approach means authentication is not integrated or shared with the rest of your website.
Which means for all but the simplest scenarios a better approach is needed: HttpModules.
HttpModules can do all sort of things, including Authentication, and have the ability to intercept all requests to the website, essentially sitting under your Data Service.
This means you can remove all authentication logic from your Data Service. And create a HttpModule to protect everything on your website - including your Data Service.
Thankfully IIS ships with a number of Authentication HttpModules:
You just need to enable the correct one and IIS will do the rest.
So by the time your request hits your Data Service the user with be authenticated.
If however you need another authentication scheme you need to create and register a custom HttpModule.
So lets take our – incredibly naive – authentication logic from Part 4 and turn it into a HttpModule.
First we need a class that implements IHttpModule, and hooks up to the AuthenticateRequest event something like this:
public class CustomAuthenticationModule: IHttpModule { public void Init(HttpApplication context) { context.AuthenticateRequest += new EventHandler(context_AuthenticateRequest); } void context_AuthenticateRequest(object sender, EventArgs e) { HttpApplication app = (HttpApplication)sender; if (!CustomAuthenticationProvider.Authenticate(app.Context)) { app.Context.Response.Status = "401 Unauthorized"; app.Context.Response.StatusCode = 401; app.Context.Response.End(); } } public void Dispose() { } }
We rely on the CustomAuthenticationProvider.Authenticate(..) method that we wrote in Part 4 to provide the actual authentication logic.
Finally we need to tell IIS to load our HttpModule, by adding this to our web.config:
<system.webServer> <modules> <add name="CustomAuthenticationModule" type="SimpleService.CustomAuthenticationModule"/> </modules> </system.webServer>
Now when we try to access our Data Service - and the rest of the website – it should be protected by our HttpModule. NOTE: If it this doesn’t work, you might have IIS 6 or 7 running in classic mode which requires slightly different configuration.
In part 2 we looked about using Windows Authentication. And in parts 3, 4 and 5 we covered all the hooks available to Authentication logic in Data Services, and discovered that pretty much everything you need to do is possible.
Great.
Next we’ll focus on real world scenarios like:
Alex JamesProgram ManagerMicrosoft
If you secure an OData Service using Windows authentication – see Part 2 to learn how – everything works as expected out of the box.
What however if you need a different authentication scheme?
Well the answer as always depends upon your scenario.
Broadly speaking what you need to do depends upon how your Data Service is hosted. You have three options:
But by far the most common scenario is…
This is what you get when you deploy your WebApplication project – containing a Data Service – to IIS.
At this point you have two realistic options:
Which is best?
Undoubtedly the ProcessingPipeline option is easier to understand and has less moving parts. Which makes it an ideal solution for simple scenarios.
But the ProcessingPipeline is only an option if it makes sense to allow anonymous access to the rest of website. Which is pretty unlikely unless the web application only exists to host the Data Service.
Nevertheless the ProcessingPipeline approach is informative, and most of the code involved can be reused if you ever need to upgrade to a fully fledged HttpModule.
So how do you use the ProcessingPipeline?
Well the first step is to enable anonymous access to your site in IIS:
Next you hookup to the ProcessingPipeline.ProcessingRequest event:
public class ProductService : DataService<Context> { public ProductService() { this.ProcessingPipeline.ProcessingRequest += new EventHandler<DataServiceProcessingPipelineEventArgs>(OnRequest); }
Then you need some code in the OnRequest event handler to do the authentication:
void OnRequest(object sender, DataServiceProcessingPipelineEventArgs e) { if (!CustomAuthenticationProvider.Authenticate(HttpContext.Current)) throw new DataServiceException(401, "401 Unauthorized"); }
In this code we call into a CustomAuthenticationProvider.Authenticate() method. If everything is okay – and what that means depends upon the authentication scheme - the request is allowed to continue.
If not we throw a DataServiceException which ends up as a 401 Unauthorized response on the client.
Because we are hosted in IIS our Authenticate() method has access to the current Request via the HttpContext.Current.Request.
My pseudo-code, which assumes some sort of claims based security, looks like this:
public static bool Authenticate(HttpContext context) { if (!context.Request.Headers.AllKeys.Contains("Authorization")) return false; // Remember claims based security should be only be // used over HTTPS if (!context.Request.IsSecureConnection) return false;
string authHeader = context.Request.Headers["Authorization"];
IPrincipal principal = null; if (TryGetPrinciple(authHeader, out principal)) { context.User = principal; return true; } return false; }
What happens in TryGetPrincipal() is completely dependent upon your auth scheme.
Because this post is about server hooks, not concrete scenarios, our TryGetPrincipal implementation is clearly NOT meant for production (!):
private static bool TryGetPrincipal( string authHeader, out IPrincipal principal) { // // WARNING: // our naive – easily mislead authentication scheme // blindly trusts the caller. // a header that looks like this: // ADMIN username // will result in someone being authenticated as an // administrator with an identity of ‘username’ // i.e. not exactly secure!!! // var protocolParts = authHeader.Split(' '); if (protocolParts.Length != 2) { principal = null; return false; } else if (protocolParts[0] == "ADMIN") { principal = new CustomPrincipal( protocolParts[1], "Administrator", "User" ); return true; } else if (protocolParts[0] == "USER") { principal = new CustomPrincipal( protocolParts[1], "User" ); return true; } else { principal = null; return false; } }
Don’t worry though as this series progresses we will look at enabling real schemes like Custom Basic Auth, OAuthWrap, OAuth 2.0 and OpenId.
Strictly speaking you don’t need to set the Current.User, you could just allow or reject the request. But we want to access the User and their roles (or claims) for authorization purposes, so our TryGetPrincipal code needs an implementation of IPrincipal and IIdentity:
public class CustomPrincipal: IPrincipal { string[] _roles; IIdentity _identity;
public CustomPrincipal(string name, params string[] roles) { this._roles = roles; this._identity = new CustomIdentity(name); }
public IIdentity Identity { get { return _identity; } }
public bool IsInRole(string role) { return _roles.Contains(role); } } public class CustomIdentity: IIdentity { string _name; public CustomIdentity(string name) { this._name = name; }
string IIdentity.AuthenticationType { get { return "Custom SCHEME"; } }
bool IIdentity.IsAuthenticated { get { return true; } }
string IIdentity.Name { get { return _name; } } }
Now my authorization logic only has to worry about authenticated users, and can implement fine grained access control.
For example if only Administrators can see products, we can enforce that in a QueryInterceptor like this:
[QueryInterceptor("Products")] public Expression<Func<Product, bool>> OnQueryProducts() { var user = HttpContext.Current.User; if (user.IsInRole("Administrator")) return (Product p) => true; else return (Product p) => false; }
In this post you saw how to add custom authentication logic *inside* the Data Service using the ProcessingPipeline.ProcessRequest event.
Generally though when you want to integrate security across your website and your Data Service, you should put your authentication logic *under* the Data Service, in a HttpModule.
More on that next time…
Today we released an OData Mailing List. This is the list to use if you have OData questions, comments on OData or want to discuss how OData should evolve over time.
To sign up to the list go here and follow the instructions provided. The list is fully open so anyone can subscribe and participate. Be sure to read the terms of use on the signup page to understand how your feedback on the mailing list may be used to enhance OData over time.
You can see a read only archive of all the messages from the mailing list here: http://www.odata.org/mailing-list
We look forward to hearing your thoughts, comments and feedback on the list!
-Mike Flasko
Data Services/OData, Lead Program Manager
TechEd North America 2010 is fast approaching (it starts Monday). This year we have added an OData service to the TechEd site, just like we did for MIX10 earlier this year. The service exposes the sessions, speakers and other associated information for the conference and is a great way to learn OData. Check-out the API page on the TechEd site here for more information on the service. If you are attending TechEd make sure you stop by the DMG booth (in the DAT section) and show the folks from DMG your OData App.
Head over to http://www.odata.org/consumers to see the list of applications and libraries that can consume the OData feed. To demonstrate what you can do with the service, I have some screen shots below of browsing the feed using the OData Explorer and finding a list of all sessions about OData – there are a bunch (make sure you run the explorer in OOB mode when browsing the service).
Program Manager II
WCF Data Services
Chris (aka Woody) Woodruff has organized a series of OData workshops to compliment the official OData Roadshow.
This is highly recommended. So if for whatever reason you can’t make it to one of the OData Roadshow events, or you just can’t get enough OData, see if you can get along to one of Chris’ workshops.
The workshops start in Raleigh tomorrow and finish in NYC on June 28th.
Learn more and register here.