Products

Solutions

Resources

Partners

Community

About

New Community Website

Ordinarily, you'd be at the right spot, but we've recently launched a brand new community website... For the community, by the community.

Yay... Take Me to the Community!

The Community Blog is a personal opinion of community members and by no means the official standpoint of DNN Corp or DNN Platform. This is a place to express personal thoughts about DNNPlatform, the community and its ecosystem. Do you have useful information that you would like to share with the DNN Community in a featured article or blog? If so, please contact .

The use of the Community Blog is covered by our Community Blog Guidelines - please read before commenting or posting.


Authorization in the new DNN Services Framework

I’ve been playing around with the new DNN Service Framework a little since the CTP 1 came out. As a module developer this is a very interesting addition to the framework (and one I’ve been asking for, for a while). In fact, I think it is the most significant change in 6.2. But then, I’ve always been into DNN because of the power of the framework.

As so many new additions to the framework, this starts out as a request from the community that then gets specced, scoped and what have you. Then we see it appear in the CTP/Beta with a note “by the way, we added feature XYZ”. Great. How does it work? And a demo comes soon after to show how it should work. Now, explaining what a new button does in the UI is one thing. Explaining how a completely new “framework” addition does is quite another. As always we begin with a “Hello World” example (as in the linked post above). But as a professional module developer you very quickly have to move beyond that. And my first stop is always: security. How does this feature open up the possibility to provide controlled access to my module’s resources? So in this post I want to move beyond the hello word example.

Goal

I’d like to build a RESTful service that allows you to retrieve the contents of Text/Html modules throughout the site. This means accessing contents as it were a tree of tabs (pages) and then modules. So “GET …/23/345” will get the module’s contents for module 345 on tab 23. If I omit the module id I should be returned a list of Text/Html modules (i.e. a list of subnodes). So “GET …/23” should get a list of Text/Html modules on tab 23. Finally if I omit the tab ID I’d like to see a list of tabs. So the base “GET” to my service should get a list of tabs. Now I’d also like this to be fully protected. So I don’t see tabs I’m not entitled to see, nor modules I’m not entitled to see.

Step 1: Create controller and first route

The first step is to open our project (in this case the Html module) and add a service controller which will do the magic. The snippet below I’ve added as “ServiceController.cs” to the Html module project.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using DotNetNuke.Web.Services;
using DotNetNuke.Security.Permissions;

namespace DotNetNuke.Modules.Html
{
 public class ServiceController : DnnController, IServiceRouteMapper
 {

  public void RegisterRoutes(ServicesRoutingManager routeManager)
  {
   routeManager.MapRoute("Html", "default", "", new { controller = "Service", action = "GetTabs" }, new[] { "DotNetNuke.Modules.Html" });
  }

  [DnnAuthorize(AllowAnonymous = true)]
  public ActionResult GetTabs()
  {
   Dictionary<string, string> tabs = new Dictionary<string, string>();
   foreach (DotNetNuke.Entities.Tabs.TabInfo t in DotNetNuke.Entities.Tabs.TabController.GetPortalTabs(PortalSettings.PortalId, -1, false, "", false, false, true, true, false))
   {
    tabs.Add(t.TabID.ToString(), t.TabName);
   }
   return Json(tabs, JsonRequestBehavior.AllowGet);
  }

 }
}

As was explained in the other post you need to (1) inherit from DnnController, (2) implement IServiceRouteMapper and then (3) Create a method RegisterRoutes which will register your module’s routes with DNN. Here my first route is called “default”, does not take anything in the query (third parameter) and instructs the routing engine to look for “ServiceController” in the “DotNetNuke.Modules.Html” namespace and find method (action) “GetTabs”. We then create this method “GetTabs” such that it returns a list of Tabs in Json. So far so good. If you compile this you’ll see something like this appear when you call your service route:

Request Response
GET DesktopModules/Html/API
{"55":"Home","57":"Module"}

This is a list of just two tabs: “Home” and “Module”. Now what about security? You’ll notice there are no admin tabs present. This is because I was not logged in and the GetPortalTabs method above includes the CheckViewPermission. If I log in as admin you’d get this:

Request Response
GET DesktopModules/Html/API
{"55":"Home","57":"Module","59":"Site Settings","60":"Pages","61":"Extensions","62":"Languages","63":"Skins","64":"Security Roles",
"65":"User Accounts","66":"Vendors","67":"Site Log","68":"Newsletters","69":"File Manager","70":"Recycle Bin","71":"Log Viewer",
"72":"Site Wizard","73":"Google Analytics","74":"Taxonomy","75":"Search Engine SiteMap"}

OK, so what is happening here? Well, we’re letting the user go to the method regardless of login status. So a response is always generated. However, when we’re logged in we receive a cookie from DNN and that gets picked up on this request and DNN knows who we are and sets the user in the context. So that is really just like with any regular web request. There is no new magic here. But it is good to know that this still works. Now we’re going to make things a little harder. For in the next step we will ask for a list of modules on a single tab that are Text/Html modules.

Step 2: Getting the modules

We need to create a second route and we need to create a new method to get the modules.

   routeManager.MapRoute("Html", "modules", "{tabid}", new { controller = "Service", action = "GetModules" }, new { tabid = @"\d*" }, new[] { "DotNetNuke.Modules.Html" });

...

  [DnnAuthorize(AllowAnonymous = true)]
  public ActionResult GetModules(int tabid)
  {
   DotNetNuke.Entities.Modules.ModuleController mc = new DotNetNuke.Entities.Modules.ModuleController();
   Dictionary<string, string> modules = new Dictionary<string, string>();
   foreach (DotNetNuke.Entities.Modules.ModuleInfo m in GetModulesByPageAndDefinition(PortalSettings.PortalId, tabid, "Text/HTML"))
   {
    if (ModulePermissionController.HasModuleAccess(Security.SecurityAccessLevel.View, "", m))
    {
     modules.Add(m.ModuleID.ToString(), m.ModuleTitle);
    }
   }
   return Json(modules, JsonRequestBehavior.AllowGet);
  }

  public List GetModulesByDefinition(int portalID, string friendlyName)
  {
   return DotNetNuke.Common.Utilities.CBO.FillCollection(DotNetNuke.Data.DataProvider.Instance().GetModuleByDefinition(portalID, friendlyName));
  }

  public IEnumerable GetModulesByPageAndDefinition(int portalID, int tabID, string friendlyName)
  {
   return GetModulesByDefinition(portalID, friendlyName).Where(x => x.TabID == tabID);
  }

The second route becomes a little more complex. It still points to this class and will look for the method “GetModules”. But it’ll only fire if there is a set of digits after our base route. These digits get put into a variable called “tabid”. This parameter is passed into our method “Modules” and we get a list of Text/Html modules which the current user has view access to. In my test site this works out as follows:

Request Response
GET DesktopModules/Html/API/57
{"382":"Test text module"}

There is just one module on this page and it’s called “Test text module” as you can see. But we omitted one thing: any user can request this route and still get an answer. Instead, I’d prefer to have access to the page checked before we even get to the code. This is done with the authorization attribute. As we saw in the mentioned post, we have a default attribute called DnnAuthorize which takes as parameters Roles, AllowAnonymous, and RequiresHost. This is the core logic for it:

   IPrincipal user = context.User;
   if (!AllowAnonymous)
   {
    if (user == null || !user.Identity.IsAuthenticated)
    {
     return false;
    }
   }

   if (RequiresHost)
   {
    if (!CurrentUser.IsSuperUser)
    {
     return false;
    }
   }

   if (_rolesSplit.Any())
   {
    if (!_rolesSplit.Any(CurrentUser.IsInRole))
    {
     return false;
    }
   }

As you can see this does some really basic authorization. The red herring here is the Roles parameter. The roles are in plain English text and so there are two reasons you’ll probably not use that parameter: (1) you rarely code for roles, instead you code for permissions, and (2) anything in text is subject to change in other languages. In case you’re confused: it is not uncommon for a French site to have removed the “Administrators” role and replaced it with “Administrateurs”. And voila, your code no longer works as expected. So the DnnAuthorize attribute is of little use in real-world authorization. What we need to do is to create our own version of an authorization attribute.

Step 3: Roll your own authorization attribute

Now we’re getting to the nitty-gritty of this. Let’s grab a spanner, follow me to the engine room and see if we can do this. The trick is this. Authorization cannot be done while you’re running the method. It is done before by the MVC framework. It took me a while to get my head round to this. So in essence you’ll be creating code that sits in another class which does just the authorization. Nothing else. I’ll create an attribute to handle the authorization of the user to the tab he/she is requesting the modules from. We’ll call this the HtmlTabAuthorize attribute:

using DotNetNuke.Common.Utilities;
using DotNetNuke.Entities.Modules;
using DotNetNuke.Entities.Modules.Internal;
using DotNetNuke.Security;
using DotNetNuke.Security.Permissions;
using DotNetNuke.Entities.Tabs;

namespace DotNetNuke.Modules.Html
{
 public class HtmlTabAuthorizeAttribute : DotNetNuke.Web.Services.AuthorizeAttributeBase
 {
  protected override bool AuthorizeCore(HttpContextBase context)
  {
   int tabid = -1;
   string url = context.Request.RawUrl;
   Match m = Regex.Match(url, "Html/API/(\\d+).*");
   if (m.Success)
   {
    Int32.TryParse(m.Groups[1].Value, out tabid);
   }
   TabInfo t = CBO.FillObject<TabInfo>(DotNetNuke.Data.DataProvider.Instance().GetTab(tabid));
   if (t != null)
   {
    return TabPermissionController.CanViewPage(t);
   }
   return false;
  }
 }
}

As you can see we have a main entry point which is a call to “AuthorizeCore” which hands us the context of the call. So that’s all we have. We need to somehow get at the tab id and it is not handed over to us automatically as with the GetModules method. What I’ve done here is to regex it out of the url. I then get the tab and subsequently let DNN decide if the current user can view that tab. It is simple enough and it illustrates how you need to carefully plan your routes.

Now we add this attribute to the GetModules method:

  [DnnAuthorize(AllowAnonymous = true)]
  [HtmlTabAuthorizeAttribute()]
  public ActionResult GetModules(int tabid)
  {
...

As you can see these attributes can be stacked. I needed to include the “let anonymous users through” bit of DnnAuthorize (I’m working with CTP 2 here). It seems superfluous, but if not included the authorization will only succeed if you’re host. The result is a neat login popup if you try to access a page you don’t have access to. Enter your DNN credentials and you’ll get the right reply.

image

So what’s with that popup? Ah. That, my friend, is the all new digest authentication kicking in. Digest? Yes, digest. Let me explain.

Intermezzo: Digest authentication

For the security buffs out there: DotNetNuke now includes its own digest authentication implementation. This is pretty cool. It means we finally have something safer than basic authentication that we can use for these services. In a nutshell the difference comes down to this. Basic authentication sends your login details over to the server in clear text. That is: your username and your password. That is an obvious security risk. The fact is that most sites run this way. Webforms and otherwise. The only way to hide the login details is to go https, but that is a bridge too far for many. Any time you enter your password on a page which is plain old http, you know it is going over the wire. And not as asterisks! So apart from https are there any alternatives? Yes, digest for instance. This authentication mechanism is based on you signing your username and password together with some unique info from the server which the server then verifies. The great thing is that only the signature makes it across. Not the password itself. And of course the signature is different for every time the conversation takes place. Want to know more:

https://en.wikipedia.org/wiki/Digest_authentication

The great thing about this is that you could now code an app (i.e. like on an iPhone) that stores your password in the device and authenticates you on your DNN site without passing this over the air. That is essential IMO to future mobile development for DNN as mobile device traffic is particularly vulnerable to snooping.

Step 4: Wiring up access to the module content

Now we apply what we know and complete our (relatively) simple module content browser. We add a route and method for the module’s content:

   routeManager.MapRoute("Html", "module", "{tabid}/{moduleid}", new { controller = "Service", action = "GetContents" }, new { tabid = @"\d*", moduleid = @"\d*" }, new[] { "DotNetNuke.Modules.Html" });

...

  [DnnAuthorize(AllowAnonymous = true)]
  [HtmlModuleAuthorizeAttribute(AccessLevel = Security.SecurityAccessLevel.View)]
  public ActionResult GetContents(int tabid, int moduleid)
  {
   var objHtmlText = (HtmlTextInfo)(DotNetNuke.Common.Utilities.CBO.FillObject(DataProvider.Instance().GetTopHtmlText(moduleid, true), typeof(HtmlTextInfo)));
   return Json(objHtmlText.Content, JsonRequestBehavior.AllowGet);
  }

And we add a new authorization attribute:

 public class HtmlModuleAuthorizeAttribute : DotNetNuke.Web.Services.AuthorizeAttributeBase
 {
  public HtmlModuleAuthorizeAttribute()
  {
   AccessLevel = SecurityAccessLevel.Host;
  }

  public SecurityAccessLevel AccessLevel { get; set; }

  protected override bool AuthorizeCore(HttpContextBase context)
  {
   var activeModule = context.FindModuleInfo();

   if (activeModule != null)
   {
    return HasModuleAccess(AccessLevel, "", activeModule);
   }

   return false;
  }

  public static bool HasModuleAccess(SecurityAccessLevel accessLevel, string permissionKey, ModuleInfo moduleConfiguration)
  {
   bool isAuthorized = false;
   UserInfo userInfo = UserController.GetCurrentUserInfo();
   if (userInfo != null && userInfo.IsSuperUser)
   {
    isAuthorized = true;
   }
   else
   {
    switch (accessLevel)
    {
     case SecurityAccessLevel.Anonymous:
      isAuthorized = true;
      break;
     case SecurityAccessLevel.View:
      if (ModulePermissionController.CanViewModule(moduleConfiguration))
      {
       isAuthorized = true;
      }
      break;
     case SecurityAccessLevel.Edit:
      if (TabPermissionController.CanAddContentToPage())
      {
       isAuthorized = true;
      }
      else
      {
       if (string.IsNullOrEmpty(permissionKey))
       {
        permissionKey = "CONTENT,DELETE,EDIT,EXPORT,IMPORT,MANAGE";
       }
       if (moduleConfiguration != null && ModulePermissionController.CanViewModule(moduleConfiguration) &&
           (ModulePermissionController.HasModulePermission(moduleConfiguration.ModulePermissions, permissionKey) || ModulePermissionController.HasModulePermission(moduleConfiguration.ModulePermissions, "EDIT")))
       {
        isAuthorized = true;
       }
      }
      break;
     case SecurityAccessLevel.Admin:
      isAuthorized = TabPermissionController.CanAddContentToPage();
      break;
     case SecurityAccessLevel.Host:
      break;
    }
   }
   return isAuthorized;
  }
 }

 public static class HttpContextExtensions
 {
  private const string ModuleIdKey = "ModuleId";
  private const string TabIdKey = "TabId";

  public static int FindTabId(this HttpContextBase context)
  {
   int res = -1;
   string url = context.Request.RawUrl;
   Match m = Regex.Match(url, "Html/API/(\\d+).*");
   if (m.Success)
   {
    Int32.TryParse(m.Groups[1].Value, out res);
   }
   return res;
  }

  public static int FindModuleId(this HttpContextBase context)
  {
   int res = -1;
   string url = context.Request.RawUrl;
   Match m = Regex.Match(url, "Html/API/\\d+/(\\d+).*");
   if (m.Success)
   {
    Int32.TryParse(m.Groups[1].Value, out res);
   }
   return res;
  }

  public static ModuleInfo FindModuleInfo(this HttpContextBase context)
  {
   var tabId = context.FindTabId();
   var moduleId = context.FindModuleId();

   if (moduleId != Null.NullInteger && tabId != Null.NullInteger)
   {
    return TestableModuleController.Instance.GetModule(moduleId, tabId);
   }

   return null;
  }
 }

This code is based on the DnnModuleAuthorizeAttribute which is in the Core. The latter, though, requires you to include ModuleId and TabId as querystring parameters. This was not the way we wanted to route this. We wanted a straight RESTful implementation. So here I’ve done the parsing out of the tab id and module id from the url directly instead of relying on the core to do this based on those parameters.

I also had to copy in and modify ModulePermissionController.HasModuleAccess as in the check for View permissions is erroneously just wants to use what it considers the context TabId. This is not set in our case so will default to the Home tab.

Request Response
GET DesktopModules/Html/API/57/382
"&lt;p&gt;This is my test content&lt;/p&gt;"

Wrapping up

It is hard to hide my enthusiasm for this new part of the framework. As a core team member I was privy to this information a long time ago, but I’ve had to sit tight for a while before I got my hands on it. In the meantime I contributed the digest authentication code to the project which I’ve used for a long time now in the WebDAV implementation of Document Exchange. I strongly believe the result is an API which opens the door to secure mobile development against third party modules in DotNetNuke. Wow. That is a future most developers are probably eager to get involved in. Now if only someone would convince Apple/Google to abandon Objective-C/Java in favour of .net …

Next step would be to implement the PUT verb. After all, we want to make this a read/write service in some point in the future. Maybe hook in your own desktop editor for your DNN. Inspired?

Comments

Ainsof So'o
Any ideas how to authenticate from outside of DNN using javascript?
Ainsof So'o Sunday, July 3, 2016 4:17 PM (link)

Comment Form

Only registered users may post comments.

NewsArchives


Aderson Oliveira (22)
Alec Whittington (11)
Alessandra Daniels (3)
Alex Shirley (10)
Andrew Hoefling (3)
Andrew Nurse (30)
Andy Tryba (1)
Anthony Glenwright (5)
Antonio Chagoury (28)
Ash Prasad (37)
Ben Schmidt (1)
Benjamin Hermann (25)
Benoit Sarton (9)
Beth Firebaugh (12)
Bill Walker (36)
Bob Kruger (5)
Bogdan Litescu (1)
Brian Dukes (2)
Brice Snow (1)
Bruce Chapman (20)
Bryan Andrews (1)
cathal connolly (55)
Charles Nurse (163)
Chris Hammond (213)
Chris Paterra (55)
Clint Patterson (108)
Cuong Dang (21)
Daniel Bartholomew (2)
Daniel Mettler (181)
Daniel Valadas (48)
Dave Buckner (2)
David Poindexter (12)
David Rodriguez (3)
Dennis Shiao (1)
Doug Howell (11)
Erik van Ballegoij (30)
Ernst Peter Tamminga (80)
Francisco Perez Andres (17)
Geoff Barlow (12)
George Alatrash (12)
Gifford Watkins (3)
Gilles Le Pigocher (3)
Ian Robinson (7)
Israel Martinez (17)
Jan Blomquist (2)
Jan Jonas (3)
Jaspreet Bhatia (1)
Jenni Merrifield (6)
Joe Brinkman (274)
John Mitchell (1)
Jon Henning (14)
Jonathan Sheely (4)
Jordan Coopersmith (1)
Joseph Craig (2)
Kan Ma (1)
Keivan Beigi (3)
Kelly Ford (4)
Ken Grierson (10)
Kevin Schreiner (6)
Leigh Pointer (31)
Lorraine Young (60)
Malik Khan (1)
Matt Rutledge (2)
Matthias Schlomann (16)
Mauricio Márquez (5)
Michael Doxsey (7)
Michael Tobisch (3)
Michael Washington (202)
Miguel Gatmaytan (3)
Mike Horton (19)
Mitchel Sellers (40)
Nathan Rover (3)
Navin V Nagiah (14)
Néstor Sánchez (31)
Nik Kalyani (14)
Oliver Hine (1)
Patricio F. Salinas (1)
Patrick Ryan (1)
Peter Donker (54)
Philip Beadle (135)
Philipp Becker (4)
Richard Dumas (22)
Robert J Collins (5)
Roger Selwyn (8)
Ruben Lopez (1)
Ryan Martinez (1)
Sacha Trauwaen (1)
Salar Golestanian (4)
Sanjay Mehrotra (9)
Scott McCulloch (1)
Scott Schlesier (11)
Scott Wilkinson (3)
Scott Willhite (97)
Sebastian Leupold (80)
Shaun Walker (237)
Shawn Mehaffie (17)
Stefan Cullmann (12)
Stefan Kamphuis (12)
Steve Fabian (31)
Steven Fisher (1)
Tony Henrich (3)
Torsten Weggen (3)
Tycho de Waard (4)
Vicenç Masanas (27)
Vincent Nguyen (3)
Vitaly Kozadayev (6)
Will Morgenweck (40)
Will Strohl (180)
William Severance (5)
What is Liquid Content?
Find Out
What is Liquid Content?
Find Out
What is Liquid Content?
Find Out