Background
With the release of DotNetNuke 7.0.0 we’re moving the service framework to use WebAPI, rather than the asp.net MVC version that was released in 6.2.0. Scott’s already covered converting any MVC services you may have created here , as well as providing an excellent blog on Getting Started with Services Framework WebAPI edition here , but we wanted to provide some further detail and some tips for developers interested in creating services for 7.0.0 and above.
There are many good examples already of full service implementations , including one from Scott’s blog and of course all the core services themselves which have been updated to use WebAPI and can be viewed in the latest source code, so this blog will only contain the server code implementation and not the caller (whilst you can call it via a server HTTPWebRequest or MS Ajax, I recommend following the examples in the core where we use jQuery ajax and KnockoutJS )
Service Implementation
To create a service framework in DotNetNuke, you need a minimum of two classes – one that controls the routing (i.e. processes the request and redirects to the controller) and one that exposes the service framework methods. These classes can be contained in either a web-site project (WSP) or web application project (WAP) – the recommendation is to use the WAP model.
If using the WAP model, open vs.net and create a new class library project with references to all the appropriate libraries.
- In Visual Studio create a new class library project for .Net Framework 4.0
- Add references to the following libraries in your installation (Browse to the /bin folder of you install using the Add Reference dialog box)
- DotNetNuke.dll
- DotNetNuke.Web.dll
- System.Net.Http.dll
- System.Net.Http.Formatting.dll
- System.Web.Http.dll
- Add references to the following standard .Net libraries (Use the .Net tab of the Add Reference dialog box)
- Set the output path of your project to the /bin folder of your 7.0 installation
- Delete the default Class1.cs (or class1.vb if using vb.net) file
- Add a new file to store the route details e.g. create a file called RouteMapper.cs and add the following code (this blog has further detail on the parameters used in the route definition)
using DotNetNuke.Web.Api;
namespace MyServices
{
public class RouteMapper : IServiceRouteMapper
{
public void RegisterRoutes(IMapRoute mapRouteManager)
{
mapRouteManager.MapHttpRoute("MyServices", "default", "{controller}/{action}", new[] { "MyServices" });
}
}
}
- Now we need to create a class that contains our service framework implementation. Create another class, in my case one called RoleSubscriptionController.cs and add the following code:
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using DotNetNuke.Entities.Portals;
using DotNetNuke.Entities.Users;
using DotNetNuke.Security.Roles;
using DotNetNuke.Web.Api;
namespace MyServices
{
public class RoleSubscriptionController : DnnApiController
{
[DnnAuthorize]
[HttpGet]
public HttpResponseMessage GetPublicRoles()
{
PortalSettings ps = PortalController.GetCurrentPortalSettings();
var rc = new RoleController();
ArrayList lstRoles = rc.GetPortalRoles(ps.PortalId);
IList results = (from RoleInfo objRole in lstRoles
where objRole.IsPublic
select new SubscribedRoles
{
RoleId = objRole.RoleID,
RoleName = objRole.RoleName,
Subscribed =
UserController.GetCurrentUserInfo().IsInRole(
objRole.RoleName)
}).ToList();
return Request.CreateResponse(HttpStatusCode.OK, results.OrderBy(sr => sr.RoleName));
}
[HttpPost]
[ValidateAntiForgeryToken]
[DnnAuthorize]
public HttpResponseMessage SetRole(RoleDTO dto)
{
PortalSettings ps = PortalController.GetCurrentPortalSettings();
var rc = new RoleController();
rc.UpdateUserRole(ps.PortalId, UserController.GetCurrentUserInfo().UserID,
dto.RoleID, dto.Subscribed);
return Request.CreateResponse(HttpStatusCode.OK);
}
#region Nested type: RoleDTO
public class RoleDTO
{
public int RoleID { get; set; }
public bool Subscribed { get; set; }
}
#endregion
}
In this example I also use another class (SubscriberRoles) to constrain the data I pass back i.e. there is not need to pass full RoleInfo instances.
using System;
namespace MyServices
{
public class SubscribedRoles
{
public int RoleId;
public string RoleName;
public Boolean Subscribed;
}
}
Note: the main thing to take away from this work is the inheritance – in the case of our routing class we ensure we inherit from IServiceRouteMapper, and in the case of our service itself it inherits from DnnApiController. This inheritance allows DotNetNuke to find and register the supported routes and to route requests to the correct service framework methods. Now we’ve got some code, lets examine it in more detail.
Checking route registration
Before you get too far along it’s wise to check that your routing is working as expected. Looking at the first 3 parameters in the mapped route we can see that the request should be in the form http://dnndev/DesktopModules/MyServices/API/RoleSubScription/GetPublicRoles , so if we enter that into the browser we should see a successful request with some data returned (in our case just the public roles that the current user is a member of).
If we examine the request in a http proxy such as Fiddler we’ll also see it returned with a HTTP status of 200, indicating a successful request. If the service doesn’t return data but instead shows up as a 404, we have an issue. If the method looks fine (e.g. it’s a HTTPGet so will work with URL based requests) and the namespaces appear correct, it’s a good idea to check and see what route is being registered in DotNetNuke. To do so, you can increase the logging level in log4net to “All” to capture additional diagnostics. Once that’s increased, go to Portals\_default\Logs and open todays log file and search for “Mapping route” – you should find the route details being logged like below:
2012-11-06 17:27:33,623 [DNN-PC25][Thread:22][TRACE] DotNetNuke.Web.Api.Internal.ServicesRoutingManager - Mapping route: MyServices-default-0 @ DesktopModules/MyServices/API/{controller}/{action}
If you don’t see the route registered then you’ve probably forgot to set the inheritance correctly, the project has errors, or is not set to deploy the dll to the bin folder, so check again. Otherwise you may have made a typo in your URL (either the “DesktopModules/MyServices” portion or the name of the controller or action, so check those again – remember that the controller is simply the name of your controller file from above with the “Controller” part dropped, so in our example it’s “RoleSubScription”)
Note: In the fall update Microsoft are releasing some support for WebAPI tracing that you’ll be able to add to your projects and I’m sure we’ll investigate adding it to the core to make the debugging experience better.
Using the correct verbs
In our example, our two public methods use different verbs. GetPublicRoles uses the HTTPGet attribute and SetRole uses HTTPPost. This is deliberate as different types of action should use different verbs.Whilst it may be tempting to use simple GET’s for all your services as they’re easier to test by using querystring parameters, this is an unsafe approach. The best practice guideline is to only use GET for a request that returns data, but to use the POST verb for a request that changes data.
The reason this is important is that a GET request can be invoked very simply via the URL or via any HTML object that supports external requests e.g. a html tag such as <img alt="" src="”<a" href="http://dnndev/DesktopModules/MyServices/API/RoleSubScription/GetPublicRoles" /> will invoke a GET request. In the same way that one site can “leech” images from another by setting the SRC attribute to a different URL from the website, service requests that support GET can also be invoked from a different site.
The danger with this is if the service request implementation changes data, then one site can affect data on another site. Even for service requests that are not open to anonymous users, these can still be executed as your browser will automatically send the relevant site cookies when making the request.
In security terms this is called a cross-site request forgery . One famous example of this came with an early version of the Netflix API where GET requests could perform various actions which changed data. http://seclists.org/fulldisclosure/2006/Oct/316 covers this in more detail, but one of the examples of this exploit was creating an image tag that would add pornographic videos to your Netflix queue if you viewed that page – obviously not an ideal implementation and a nasty surprise for the users.
Note: whilst GET and POST are the two most commonly used verbs there are others, particularly if you are looking to use a RESTful approach. Typically a REST based service implementation will use the following verbs:
- GET - Used when a browser is requesting data from the server
- PUT - Typically used when updating an item
- POST - Typically used when creating a new item
- DELETE - Used when deleting an item
Data transfer objects
In our example above, you’ll notice that the second class (the service implementation) has another class imbedded within it (RoleDTO). This is a change in WebAPI from the previous ASP.NET MVC approach. ASP.NET MVC did not apply strict behaviors to parameters passed to a service, which allowed for POST requests to contain data in the querystring. Obviously parameters that change data should not be contained in the URL as this makes it much easier to steal or replay the actions.
One of WebAPI’s changes is to enforce parameter binding – by taking this approach the HTTP request is converted into a .net type (in our example RoleDTO) which can then be used without messy casting and provides for a better contract. This is often referred to as a “model” class, particularly in MVC, and another common name for it is a data-transfer object (or DTO) . By convention in the core we prepend these classes with DTO which indicates its purpose. You can read more on the details of parameter binding here.
Don’t pass unnecessary parameters
In our example code we are changing the state of the users roles for the portal. As such it would seem sensible to have values such as the userID and portalID, as we will need those to apply the actions, as well as the roleID and the current state of the role. However this approach is wrong, as it is not difficult for a user to create a request for themselves and capture that request in a http proxy such as Fiddler , before amending the values (e.g. I change the userID value passed to the service request to a different one such as “1” and update the superuser account)
Whilst it’s entirely possible to validate the values within the method implementation (and in cases where you’re creating a function where one user can invoke actions for another user), in general you want to not pass those parameters. Instead you should retrieve the values from the DotNetNuke API – in our case we get the current userID via UserController.GetCurrentUserInfo().UserID and the current portalID via PortalController.GetCurrentPortalSettings().PortalID.
Method authorization
In both ASP.NET MVC and WebAPI, service methods are open by default. This means that any user (including anonymous users) can call them. In DotNetNuke we’ve taken a more pessimistic approach and assumed that only Host (superuser) users can call a method unless otherwise directed (i.e. all methods have the equivalent of the RequireHost attribute). As our services are designed to be used by users or a portal, we need to indicate this – in our example we have done so by using the DnnAuthorize attribute.
Note: you can of course apply this at the class level, but in cases where your service has many methods you may want to apply it at the method level in each case as it’s easier to assess the authorization at a glance without having to scroll up to the top of the class file.
What’s ValidateAntiForgeryToken?
When working with service requests, it’s important to realize that some of the automatic security you get from asp.net Webforms is not in place. In Webforms if I have a page with a server control (such as a LinkButton) to invoke an action, when the page loads on a postback a number of pieces of validation occur – asp.net verifies if that button is able to invoke that event as well as verifying if any data was tampered with, before executing the action. This ensures that the request came from the page itself and not from another site attempting to fake the action, and that the data is valid (e.g. if selecting a value from a dropdownlist, that the value was there when the list was rendered and not “hacked-in” afterwards)
When invoking service requests via client script (such as jQuery Ajax), these automatic verifications don’t happen, so additional care must be taken. To give an example, imagine a potential hacker creates a page on their site (hackersite.com) and manages to convince you to visit their site and click on a button (you may not even know this occurs in the case of a ClickJacking attack). Their site may contain a form that posts data to your site via the action parameter e.g.
<form name="”hacker”" title="http://dnndev/DesktopModules/MyServices/API/RoleSubScription/SetRole" action="”<a" href="http://dnndev/DesktopModules/MyServices/API/RoleSubScription/SetRole">
http://dnndev/DesktopModules/MyServices/API/RoleSubScription/SetRole”>
…rest of form
This is another form of cross-site request forgery that works against services that use POST. Whilst it would seem possible to check the referral URL making the POST request to ensure it comes from your site, this is not a secure approach as this value can be amended by a number of tools (such as Fiddler). Instead your service request method should use ValidateAntiForgeryToken, which will add a cookie to the request which will then be checked before the method is executed. As the cookies scope is set to the current website this ensures that requests from other websites automatically fail.
Scotts blog covers how to set the relevant values.