Managing External Identities in Umbraco BackOffice with PolicyServer

Feature

The authors of IdentityServer did a great job providing us with a framework for incorporating identity and access control logic in our apps and APIs. But they also warned us about misusing the IdentityServer software as an authorization/permission management system. So now they have created a new product called PolicyServer and it is available in both Open Source version and a commercial product. I decided to take PolicyServer for a spin and what better way to do this than in conjunction with IdentityServer 😊

The described setup is basically an extension to this original post: Login to Umbraco BackOffice using IdentityServer

For real business scenarios: take a look at their commercial product (a big brother to the Open Source version of Policy Server) https://solliance.net/products/policyserver

Goal: Login to Umbraco BackOffice using IdentityServer and have PolicyServer define our roles in Umbraco CMS.

Setting the stage

Umbraco BackOffice allows users to login using external Identity Providers. Upon successful authentication it will create a local user and (by default) add the user to the built-in “Editor” group. In our scenario we would like to maintain the users roles separately and have these roles reflect the group membership in Umbraco BackOffice.

Umbraco supports this scenario by allowing us to extend the process of user creation (this process is known as AutoLink) allowing us to add our own logic. So we would like to end up with the following process:

PolicyServer

  • The user accesses the Umbraco BackOffice and gets redirected to the Login page;
  • This login page shows a button for the external identity provider (in our case IdentityServer);
  • When an access token is received, Umbraco uses AutoLink to create this BackOffice User;
  • Directly after the AutoLink we fetch the roles from our PolicyServer API;
  • The API returns the information and our logic (“Enroll User”) takes care of the right group membership(s).

So, we need a couple of things: Umbraco 7, IdentityServer 4 and PolicyServer.Local

Highlevel steps:

  1. Setup (install) IdentityServer through Nuget in Visual Studio;
  2. Follow the Quick Start mentioned above and add the QuickStart UI;
  3. Run IdentityServer4 by adding our configuration;
  4. Setup (install) PolicyServer.Local through Nuget in Visual Studio;
  5. Add our application policy;
  6. Setup Umbraco;
  7. Configure Umbraco BackOffice to support an external Identity Provider;
  8. Extend the AutoLink process to enroll the logged in user.

Setup IdentityServer

The first part is pretty easy and documented by the IdentityServer4 documentation. Just a couple of things we need for our setup to keep in mind:

  • Follow the steps described here: http://docs.identityserver.io/en/release/quickstarts/0_overview.html
  • We use the Visual Studio template for an empty ASP.NET Core Web Application and the Nuget package for IdentityServer4.
  • Additionally we add the Quickstart UI https://github.com/IdentityServer/IdentityServer4.Quickstart.UI that contains MVC Views and Controllers for application logic (Account Login, Logout, etc.). Make sure you review your actual requirements before taking this solution to production;
  • The following Nuget package is needed: IdentityServer4;
  • You can follow all the steps from the mentioned documentation/ quickstart. We will configure our specific client needs in the next steps;
  • We run the blogpost demo code on “InMemory” stores, needless to say this is not suitable for production.

Configure IdentityServer

After finishing the initial setup we need to configure the IdentityServer.

  • Configure clients;
  • Configure identity Resources;
  • Configure API Resources (our PolicyServer API);
  • Configure test users;
  • Configure service startup.

For the purpose of this post we create everything through code. There is also documentation on the IdentityServer4 project site that enables configuration through Entity Framework databases.

We start with separate class files to store all of our configuration. The files should contain the parts mentioned above, please see the GitHub repo for full source code:

public static IEnumerable GetClients()
{
// Please see the code in the repo  
}

public static IEnumerable GetIdentityResources()
{
// Please see the code in the repo
}

public static IEnumerable GetApiResources()
{
// Please see the code in the repo
}

public static List GetUsers()
{
// Please see the code in the repo
}

And finally the startup configuration:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddInMemoryIdentityResources(Config.MyIdentityResources.GetIdentityResources())
        .AddInMemoryApiResources(Config.MyApiResources.GetApiResources())
        .AddInMemoryClients(Config.MyClients.GetClients())
        .AddTestUsers(Config.MyUsers.GetUsers());
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseStaticFiles();
    app.UseIdentityServer();
    app.UseMvcWithDefaultRoute();
}

If you now run this project, you should be able to access a couple of URLs to test the settings. Remember that you can override the port through the Kestrel Builder Options.

Setting up our PolicyServer API

We use the Visual Studio template for an ASP.NET Core Web API Web Application and the Nuget packages PolicyServer.Local and IdentityServer4.AccessTokenValidation. The idea is, that we run the PolicyServer Client from our own “Policy API”. This might not be the ideal production scenario, but I think keeping it separate from IdentityServer is the right way to go.

See the http://policyserver.io site for what the authors of IdentityServer and PolicyServer have to say about the separation of authentication and authorization for a single application.
First step is to add the required Nuget packages to our freshly created API application:

  • Install-Package PolicyServer.Local
  • Install-Package IdentityServer4.AccessTokenValidation

Configure Startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvcCore()
        .AddAuthorization()
        .AddJsonFormatters();

    // Load the PolicyServer policies
    services.AddPolicyServerClient(Configuration.GetSection("Policy"));

    // IdentityServer Access Token Validation
    services.AddAuthentication("Bearer")
        .AddIdentityServerAuthentication(options =>
        {
            options.Authority = "http://localhost:5000";
            options.RequireHttpsMetadata = false;
            options.ApiName = "application.policy";
        });
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseAuthentication();

    // This claims augmentation middleware maps the user's authorization data into claims
    app.UsePolicyServerClaimsTransformation();
    app.UseMvc();
}

There are many ways to integrate the PolicyServer client in your application. In this case I use the Claims Transformation that maps the user’s authorization data into claims. Other use cases are well documented in the PolicyServer documentation.

Now add a simple controller that has the Authorize attribute so we end up with a User Context and the augmented claims from the PolicyServer. Our default Get route will return a JsonResult containing our claims as roles:

[Route("api/[controller]")]
[Authorize]
public class PoliciesController : Controller
{
    // GET api/policies
    [HttpGet()]
    public IActionResult Get()
    {
        var roles = User.FindAll("role");
        if (roles == null)
            return BadRequest();

        var result = new JsonResult(from r in roles select new { r.Type, r.Value });
        if (result != null)
            return Ok(result);
        else
            return NotFound();
    }
}

Our Appsettings.json should now contain our authorization policy:

{
  "Policy": {
    "roles": [
      {
        "name": "editor",
        "subjects": [ "1" ]
      },
      {
        "name": "administrator",
        "subjects": [ "2", "3" ]
      }
    ]
  }
}

Setup Umbraco

Setting up Umbraco is the final piece of the puzzle. There are detailed instructions found on the Umbraco Docs Web Site: https://our.umbraco.org/documentation/getting-started/setup/install/install-umbraco-with-nuget but we should start with a new project based on an Empty ASP.NET Web Application .NET Framework (4.6.1).
In addition we add the following Nuget packages:

  • UmbracoCms,
  • IdentityModel,
  • UmbracoCms.IdentityExtensions,
  • Microsoft.Owin.Security.OpenIdConnect

    This should give us all the plumping we need and the first thing we need to do is hookup OWIN to enable the External Identity Provider for our BackOffice Users:
    UmbracoCustomOwinStartup.cs (located in the App_Start):
var identityOptions = new OpenIdConnectAuthenticationOptions
{
    ClientId = "u-client-bo",
    SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType,
    Authority = "http://localhost:5000",
    RedirectUri = "http://localhost:5003/umbraco",
    PostLogoutRedirectUri = "http://localhost:5003/umbraco",
    ResponseType = "code id_token token",
    Scope = "openid profile email application.profile application.policy"
};

// Configure BackOffice Account Link button and style
identityOptions.ForUmbracoBackOffice("btn-microsoft", "fa-windows");
identityOptions.Caption = "OpenId Connect";

// Fix Authentication Type
identityOptions.AuthenticationType = "http://localhost:5000";

// Configure AutoLinking
identityOptions.SetExternalSignInAutoLinkOptions(new ExternalSignInAutoLinkOptions(
    autoLinkExternalAccount: true,
    defaultUserGroups: null,
    defaultCulture: null
    ));

identityOptions.Notifications = new OpenIdConnectAuthenticationNotifications
{
    SecurityTokenValidated = EnrollUser.GenerateIdentityAsync
};

app.UseOpenIdConnectAuthentication(identityOptions);

The EnrollUser.GenerateIdentityAsync contains all the code to transform the needed claims, get the roles from the PolicyServer API and eventually AutoLink the user. The github repo contains all the code, but these are the important parts:

// Call PolicyServer API
var policyClient = new HttpClient();
policyClient.SetBearerToken(notification.ProtocolMessage.AccessToken);

// Get the Roles
var response = await policyClient.GetAsync(new Uri("http://localhost:5001/api/policies"));
if (!response.IsSuccessStatusCode)
{
    Console.WriteLine(response.StatusCode);
}
else
{
    var content = await response.Content.ReadAsStringAsync();
    var roles = JObject.Parse(content)["value"];

    // Pass roles result from PolicyServer
    if (roles != null)
        RegisterUserWithUmbracoRole(userId.Value, roles, notification.Options.Authority);
}
// If we find an administrator we need to update the Umbraco Role
var roleObject = roles.FirstOrDefault(r => r["value"] != null && r["value"].ToString() == "administrator");

if (roleObject == null)
    return;

// Add User to Admin Group
var userGroup = ToReadOnlyGroup(userService.GetUserGroupByAlias("admin"));

if (userGroup == null)
    return;

umbracoUser.AddGroup(userGroup);
userService.Save(umbracoUser);

OWIN Startup
To have Umbraco BackOffice pickup our custom OWIN Startup Class, we edit the web.config:

owin

Change the appSetting value in the web.config called “owin:appStartup” to be “UmbracoCustomOwinStartup”.
That’s it, time to test!

So we head over to Umbraco BackOffice and go for the External Login:

Login

LoginIdS

After logging in, we can see Bob is indeed an Administrator with the Umbraco CMS…. way to go Bob!!

BackOffice

That’s it, please see the GitHub repo for more information.

/Y.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s