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:
- 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:
- Setup (install) IdentityServer through Nuget in Visual Studio;
- Follow the Quick Start mentioned above and add the QuickStart UI;
- Run IdentityServer4 by adding our configuration;
- Setup (install) PolicyServer.Local through Nuget in Visual Studio;
- Add our application policy;
- Setup Umbraco;
- Configure Umbraco BackOffice to support an external Identity Provider;
- 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.
- http://localhost:5000 and this should render the QuickStart UI homepage
- http://localhost:5000/.well-known/openid-configuration and that should show all the OpenID Connect endpoints
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:
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:
After logging in, we can see Bob is indeed an Administrator with the Umbraco CMS…. way to go Bob!!
That’s it, please see the GitHub repo for more information.
/Y.