Headless Episerver? Meet JOS.Epi.ContentApi

Headless Episerver? Meet JOS.Epi.ContentApi

I read this blog post by Mathias Kunto where he says that he will leave my beloved library behind and start using the new Episerver Headless API instead.

I felt like the picture above :(

I couldn't stand it. So, I decided to create a new library; meet JOS.Epi.ContentApi..
JOS.Epi.ContentApi uses JOS.ContentSerializer by default(this can be changed by swapping out the IContentApiSerializer) so you don't need to have Episerver Find to use it.

I haven't had the time to try out the new Episerver version so I don't know how well it works, but I noticed this quote from Mathias:

I had a quick look at the API functionality, and it seems like you get pretty JSON requesting URLs like /api/episerver/v1.0/content/3 and so on. However, what I really wanted was friendly URLs delivering JSON for the page in question. For instance, requesting /en/alloy-plan/download-alloy-plan/start-downloading/ would give me JSON for the Start Downloading page.

I don't like that you must know the ContentReference to fetch the JSON data, it would be better if it worked like Mathias wants it to work.

I've built that.

How to use it

  1. Install-Package Jos.Epi.ContentApi(Normal nuget, not episerver feed)
  2. Set your accept header to "application/json" and make a GET request to your desired page. Note: When the package gets installed, a transform to your web.config will be applied and add the following line:
    <add name="ContentApiModule" type="JOS.Epi.ContentApi.ContentApiModule, JOS.Epi.ContentApi" xdt:Transform="Insert" /> If it doesn't work, add it yourself at/configuration/system.webServer/modules/
  3. Profit.

Note, by default(this can be changed) the library will only serialize the response if the Accept header contains ONE value, not two, not empty, one.
If you want to change this behaviour, just swap out the IShouldSerializeResponseStrategy interface.

You can also customize when/if the serialization should take place in the same method.

Demo

This is version 1, I've already started working on filtering and stuff like that, stay tuned!

As always, the code can be found on Github.

Beware of Enum.TryParse

What do you think Enum.TryParse will return when running this code?

public enum ContactMethod  
{
   Unknown = 0,
   Email = 1,
   Phone = 2
}

var result = Enum.TryParse("10", out ContactMethod contactMethod);  

"It will return false of course, 10 is not a valid value!!"

Wrong. It will return true. And even worse, the output variable contactMethod will have the value of...10!

Don't believe me? See it for yourself (dotnetfiddle)

Now, imagine that some developer wrote the following program:

public class Program  
{
    public static void Main(string args[])
    {
        var result = Enum.TryParse(args[0], out NukeStatus nukeStatus);
        FireNuke((int)nukeStatus);
    }

    public static void FireNuke(int status)
    {
        if(status == 0)
        {
           return;
        }

        if(status > 0 && status <= 10)
        {
           Console.WriteLine("TEST FIRING");
           TestFire();
        }

        if(status >= 15)
        {
           Console.WriteLine("NUKE EM ALL!");
           NukeEmAll();
        }
    }
}

public enum NukeStatus  
{
   Idle = 0,
   TestFireOneMissile = 5,
   TestFireAllMissiles = 10,
   FireOneMissile = 15,
   FireAllMissiles = 20
}

Now imagine that someone with fat fingers should do a test run and slips on the keyboard, so instead of passing in 10, 100 will be passed in instead.

dotnet run TrumpNukeProgram 100  

BOOM

Now, I know that my example is really stupid and the code is really bad, but still, it could happen!

What to use instead of Enum.TryParse then?

Note, this is only a problem when you try to pass in numeric values to TryParse.
If you want to be sure that your (int)value really exists in the Enum, you could use Enum.IsDefined instead.
Something like this:

var nukeStatusAsString = "100";  
var myNukeStatus = int.Parse(nukeStatusAsString); // Yeah yeah, error checking I know.  
var isDefined = Enum.IsDefined(typeof(NukeStatus), myNukeStatus);  
if (!isDefined)  
{
    return NukeStatus.Idle;
}
return (NukeStatus)myNukeStatus;  

You can read more about this here (Stackoverflow) and here (Microsoft).

How to configure Episerver to use Active Directory

This is a quick guide showing how to configure Episerver to use Active Directory instead of Multiplexing/WindowsProvider. This is NOT showing how to use Azure AD.
Im testing this on a new Alloy site running Episerver 11.3.1.

NOTE, while this is a quick way of adding Active Directory support, I still recommend using ADFS or something similar instead.

Some good resources if you want to read more about this topic:

Active Directory

My AD setup.

  • Domain: local.josef.guru
  • AD Service account: [email protected], this is a normal user account responsible for connecting to the AD, used in the Membership/RoleProvider section in Web.config.
  • AD user in correct OU. In my case I will name my user josefweb and the OU will be Web
  • AD Groups named CmsAdmins and CmsEditors(you can name these groups whatever you want).

Here's an image of how my AD is setup, it's pretty standard, for this guide I've added a new OU(Organizational Units) named Web where I will add all users who will be able to access the Edit interface.

Adding new user to AD

The users also needs to be a member of the correct groups to be able to login. I will add my user to the group CmsAdmins which means that this user will be able to do everything in the cms/admin(because we will map this role to the virtual role CmsAdmins further down).

Adding correct A group to the user

Web.Config

Membership/roleprovider

Change the <membership> section to the following

<membership defaultProvider="ActiveDirectoryMembershipProvider" userIsOnlineTimeWindow="10" hashAlgorithmType="HMACSHA512">  
    <providers>
      <clear />
      <add name="ActiveDirectoryMembershipProvider"
          type="System.Web.Security.ActiveDirectoryMembershipProvider, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
          connectionStringName="ActiveDirectoryProviderConnection"
          connectionUsername="[email protected]"
          connectionPassword="mypassword"
          enableSearchMethods="true"
          attributeMapUsername="sAMAccountName" />
    </providers>
</membership>  

Change the <rolemanager> section to the following

<roleManager enabled="true" defaultProvider="ActiveDirectoryRoleProvider" cacheRolesInCookie="true">  
    <providers>
        <clear />
        <add name="ActiveDirectoryRoleProvider"
         type="EPiServer.Security.ActiveDirectoryRoleProvider, EPiServer.Cms.AspNet, Version=11.3.1.0, Culture=neutral, PublicKeyToken=8fe83dea738b45b7"
         connectionStringName="ActiveDirectoryProviderConnection"
         connectionUsername="[email protected]"
         connectionPassword="mypassword"
         attributeMapUsername="sAMAccountName" />
    </providers>
</roleManager>  

Connectionstring

Add a new connectionstring, ActiveDirectoryProviderConnection

<connectionStrings>  
    <add name="ActiveDirectoryProviderConnection" connectionString="LDAP://local.josef.guru/OU=Web,DC=local,DC=josef,DC=guru" />
    ....
</connectionStrings>  

If you are unsure about how your LDAP connectionstring should look, read this.

Virtual roles

Map the virtual roles, we want to map our AD groups CmsAdmins/CmsEditors to the virtual roles CmsAdmins and CmsEditors, this is achieved by populating the roles attribute. I've also removed the default WebAdmins, WebEditors and Administrators roles.

<virtualRoles addClaims="true">  
    <providers>
        ...
        <add name="CmsAdmins" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="CmsAdmins" mode="Any" />
        <add name="CmsEditors" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="CmsAdmins, CmsEditors" mode="Any" />
        ...
    </providers>
</virtualRoles>  

Location sections

You will need to edit the authorization section under the following locations:

  • EPiServer
  • EPiServer/CMS/admin

It should look like this <allow roles="CmsAdmins, CmsEditors" />

Example:

<location path="EPiServer">  
    <system.web>
        ...     
        <authorization>
            <allow roles="CmsAdmins, CmsEditors" />
            <deny users="*" />
        </authorization>
        ...

You should now be able to login!

Login with AD credentials