This article shows how authorization policies can be used together with IdentityServer4. The policies are configured on the resource server and the ASP.NET Core IdentityServer4 configures the user claims to match these. The resource server is also setup to encrypt a ‘Description’ field in the SQLite database, so it cannot be read by opening the SQLite database directly.
The demo uses and extends the existing code from this previous article:
OAuth2 Implicit Flow with Angular and ASP.NET Core 1.0 IdentityServer4
Code: https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow
Extending the Resource Server with Policies
An authorization policy can be added to the MVC application in the Startup class ConfigureServices method. The policy can be added globally using the filters or individually using attributes on a class or method. To add a global authorization policy, the AuthorizationPolicyBuilder helper method can be used to create the policy. The claimType parameter in the RequireClaim method must match a claim supplied in the token.
Then the policy can be added using the AuthorizeFilter in the AddMVC extension.
var guestPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .RequireClaim("scope", "dataEventRecords") .Build(); services.AddMvc(options => { options.Filters.Add(new AuthorizeFilter(guestPolicy)); });
To create policies which can be used individually, the AddAuthorization extension method can be used. Again the claimType parameter in the RequireClaim method must match a claim supplied in the token.
services.AddAuthorization(options => { options.AddPolicy("dataEventRecordsAdmin", policyAdmin => { policyAdmin.RequireClaim("role", "dataEventRecords.admin"); }); options.AddPolicy("dataEventRecordsUser", policyUser => { policyUser.RequireClaim("role", "dataEventRecords.user"); }); });
To use and authenticate the token from IdentityServer4, the UseJwtBearerAuthentication extension method can be used in the Configure method in the Startup class of the MVC application.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap = new Dictionary<string, string>(); app.UseJwtBearerAuthentication(options => { options.Authority = "https://localhost:44345"; options.Audience = "https://localhost:44345/resources"; options.AutomaticAuthenticate = true; options.AutomaticChallenge = true; });
The authorization policies can then be applied in the controller using authorization filters with the policy name as a parameter. Maybe it’s not a good idea to define each method with a different policy as this might be hard to maintain over time, maybe per controller might be more appropriate, all depends on your requirements. It pays off to define your security strategy before implementing it.
[Authorize] [Route("api/[controller]")] public class DataEventRecordsController : Controller { [Authorize("dataEventRecordsUser")] [HttpGet] public IEnumerable<DataEventRecord> Get() { return _dataEventRecordRepository.GetAll(); } [Authorize("dataEventRecordsAdmin")] [HttpGet("{id}")] public DataEventRecord Get(long id) { return _dataEventRecordRepository.Get(id); }
Adding the User Claims and Client Scopes in IdentityServer4
The resource server has been setup to check for claim types of ‘role’ with the value of ‘dataEventRecords.user’ or ‘dataEventRecords.admin’. The application is also setup to check for claims type ‘scope’ with the value of ‘dataEventRecords’. IdentityServer4 needs to be configured for this. The scope is configured for the client in the IdentityServerAspNet5 project.
new Scope { Name = "dataEventRecords", DisplayName = "Data Event Records Scope", Type = ScopeType.Resource, Claims = new List<ScopeClaim> { new ScopeClaim("role"), new ScopeClaim("dataEventRecords") } },
Then users are also configured and the appropriate role and scope claims for each user. (And some others which aren’t required for the angular client.) Two users are configured, damienboduser and damienbodadmin. The damienboduser has not the ‘dataEventRecords.admin’ role claim. This means that the user cannot create or update a user, only see the list. (Configured in the MVC Controller of the resource server)
new InMemoryUser{Subject = "48421157", Username = "damienbodadmin", Password = "damienbod", Claims = new Claim[] { new Claim(Constants.ClaimTypes.Name, "damienbodadmin"), new Claim(Constants.ClaimTypes.GivenName, "damienbodadmin"), new Claim(Constants.ClaimTypes.Email, "damien_bod@hotmail.com"), new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(Constants.ClaimTypes.Role, "admin"), new Claim(Constants.ClaimTypes.Role, "dataEventRecords.admin"), new Claim(Constants.ClaimTypes.Role, "dataEventRecords.user"), new Claim(Constants.ClaimTypes.Role, "dataEventRecords") } }, new InMemoryUser{Subject = "48421158", Username = "damienboduser", Password = "damienbod", Claims = new Claim[] { new Claim(Constants.ClaimTypes.Name, "damienboduser"), new Claim(Constants.ClaimTypes.GivenName, "damienboduser"), new Claim(Constants.ClaimTypes.Email, "damien_bod@hotmail.com"), new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(Constants.ClaimTypes.Role, "user"), new Claim(Constants.ClaimTypes.Role, "dataEventRecords.user"), new Claim(Constants.ClaimTypes.Role, "dataEventRecords") } }
The security for the client application is configured to allow the different scopes which can be used. If the client requests a scope which isn’t configured here, the client application request will be rejected.
new Client { ClientName = "angularclient", ClientId = "angularclient", Flow = Flows.Implicit, RedirectUris = new List<string> { "https://localhost:44347/identitytestclient.html", "https://localhost:44347/authorized" }, PostLogoutRedirectUris = new List<string> { "https://localhost:44347/identitytestclient.html", "https://localhost:44347/authorized" }, AllowedScopes = new List<string> { "openid", "email", "profile", "dataEventRecords", "aReallyCoolScope", "role" } },
If the user damienboduser sends a HTTP GET request for the single item, the resource server returns a 403 with no body. If the AutomaticChallenge option in the UseJwtBearerAuthentication is false, this will be returned as a 401.
Using Data Protection to encrypt the SQLite data
It is really easy to encrypt your data using the Data Protection library from ASP.NET Core. To use it in an MVC application, just add it in the ConfigureServices method using the DataProtection extension methods. There are a few different ways to configure this and it is well documented here.
This example uses the file system with a self signed cert as data protection configuration. This will work for long lived encryption and is easy to restore. The ProtectKeysWithCertificate method does not work in dnxcore at present, but hopefully this will be implemented in RC2. Thanks to Barry Dorrans @blowdart for all his help.
public void ConfigureServices(IServiceCollection services) { var folderForKeyStore = Configuration["Production:KeyStoreFolderWhichIsBacked"]; var cert = new X509Certificate2(Path.Combine(_environment.ApplicationBasePath, "damienbodserver.pfx"), ""); services.AddDataProtection(); services.ConfigureDataProtection(configure => { configure.SetApplicationName("AspNet5IdentityServerAngularImplicitFlow"); configure.ProtectKeysWithCertificate(cert); // This folder needs to be backed up. configure.PersistKeysToFileSystem(new DirectoryInfo(folderForKeyStore)); });
Now the IDataProtectionProvider interface can be injected in your class using constructor injection. You can then create a protector using the CreateProtector method. Care should be taken on how you define the string in this method. See the documentation for the recommendations.
private readonly DataEventRecordContext _context; private readonly ILogger _logger; private IDataProtector _protector; public DataEventRecordRepository(IDataProtectionProvider provider, DataEventRecordContext context, ILoggerFactory loggerFactory) { _context = context; _logger = loggerFactory.CreateLogger("IDataEventRecordResporitory"); _protector = provider.CreateProtector("DataEventRecordRepository.v1"); }
Now the protector IDataProtectionProvider can be used to Protect and also Unprotect the data. In this example all descriptions which are saved to the SQLite database are encrypted using the default data protection settings.
private void protectDescription(DataEventRecord dataEventRecord) { var protectedData = _protector.Protect(dataEventRecord.Description); dataEventRecord.Description = protectedData; } private void unprotectDescription(DataEventRecord dataEventRecord) { var unprotectedData = _protector.Unprotect(dataEventRecord.Description); dataEventRecord.Description = unprotectedData; }
When the SQLite database is opened, you can see that all description fields are encrypted.
Links
Announcing IdentityServer for ASP.NET 5 and .NET Core
https://github.com/IdentityServer/IdentityServer4
https://github.com/IdentityServer/IdentityServer4.Samples
https://github.com/aspnet/DataProtection
ASP.NET Core 1 – Authorization using Policies
http://docs.asp.net/en/latest/security/data-protection/index.html
OAuth2 Implicit Flow with Angular and ASP.NET Core 1.0 IdentityServer4
The State of Security in ASP.NET 5 and MVC 6: OAuth 2.0, OpenID Connect and IdentityServer
http://capesean.co.za/blog/asp-net-5-jwt-tokens/
https://github.com/tjoudeh/AngularJSAuthentication
