Authorization Policies and Data Protection with IdentityServer4 in ASP.NET Core

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

History:

2017-08-15: Updated ASP.NET Core 2.0, Angular 4.3.6

Full history:
https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow#history

Other posts in this series:

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 IdentityResource and the ApiResource is configured for the client in the IdentityServer AspNetCore project.

public static IEnumerable<IdentityResource> GetIdentityResources()
{
	return new List<IdentityResource>
	{
		new IdentityResources.OpenId(),
		new IdentityResources.Profile(),
		new IdentityResources.Email(),
		new IdentityResource("dataeventrecordsscope",new []{ "role", "admin", "user", "dataEventRecords", "dataEventRecords.admin" , "dataEventRecords.user" } ),
		new IdentityResource("securedfilesscope",new []{ "role", "admin", "user", "securedFiles", "securedFiles.admin", "securedFiles.user"} )
	};
}

public static IEnumerable<ApiResource> GetApiResources()
{
	return new List<ApiResource>
	{
		new ApiResource("dataEventRecords")
		{
			ApiSecrets =
			{
				new Secret("dataEventRecordsSecret".Sha256())
			},
			Scopes =
			{
				new Scope
				{
					Name = "dataeventrecordsscope",
					DisplayName = "Scope for the dataEventRecords ApiResource"
				}
			},
			UserClaims = { "role", "admin", "user", "dataEventRecords", "dataEventRecords.admin", "dataEventRecords.user" }
		},
		new ApiResource("securedFiles")
		{
			ApiSecrets =
			{
				new Secret("securedFilesSecret".Sha256())
			},
			Scopes =
			{
				new Scope
				{
					Name = "securedfilesscope",
					DisplayName = "Scope for the securedFiles ApiResource"
				}
			},
			UserClaims = { "role", "admin", "user", "securedFiles", "securedFiles.admin", "securedFiles.user" }
		}
	};
}

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.

dataprotectionAspNet5IdentityServerAngularImplicitFlow_02

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.

Note: For this to work the cert needs to be added to the cert store.

public void ConfigureServices(IServiceCollection services)
{

var connection = Configuration["Production:SqliteConnectionString"];
var folderForKeyStore = Configuration["Production:KeyStoreFolderWhichIsBacked"];

var cert = new X509Certificate2(Path.Combine(_env.ContentRootPath, "damienbodserver.pfx"), "");

// Important The folderForKeyStore needs to be backed up.
services.AddDataProtection()
	.SetApplicationName("AspNet5IdentityServerAngularImplicitFlow")
	.PersistKeysToFileSystem(new DirectoryInfo(folderForKeyStore))
	.ProtectKeysWithCertificate(cert);

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.

dataprotectionAspNet5IdentityServerAngularImplicitFlow_01png

Links

NDC London 2016 Wrap-up

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

Advertisements

19 comments

  1. […] Authorization Policies and Data Protection with IdentityServer4 in ASP.NET Core – Damian Bowden […]

  2. […] Authorization Policies And Data Protection With Identityserver4 In Asp.net Core […]

  3. […] Authorization policies and data protection with Identity Server 4 in ASP.NET Core : Damien Bod 이 ASP.NET Core 에서 Identity Server 4 를 이용한 인증 정책과 데이터 보호에 대한 글을 작성하였습니다. […]

  4. […] Authorization policies and data protection with Identity Server 4 in ASP.NET Core : Damien Bod 이 ASP.NET Core 에서 Identity Server 4 를 이용한 인증 정책과 데이터 보호에 대한 글을 작성하였습니다. […]

  5. Why is that in every sample or blog post I see, the Authority and Audience matches the Identity Server URLs? Isn’t the Audience the intended Resource server, or API server? Unless you are calling the UserInfo endpoint in the same Ids this Audience should be something like “api.domain.com”

    1. Hi Zap

      Thanks for your comment. The Audience value needs to match the aud value sent in the access_token. This value returned by IdentityServer is the default value created by IdentityServer when creating the access token. This could be changed, but it needs to match.

      Greetings Damien

  6. Hi Damien
    I realize this tutorial is almost a year old – eternity for both ASP.NET Core and IdentityServer4. So if it’s obsolete – I apologize. But I also see a comment “2016.12.04: Updated to IdentityServer4 rc4” – so I am hopeful 🙂

    You use app.UseJwtBearerAuthentication(options => { … } ) call. Is it still correct? I couldn’t find any mention of UseJwtBearerAuthentication in IdentityServer4 docs, and while the extension method is valid, it’s parameter is JwtBearerOptions, and not lambda, like in your example.

    And while the fields in JwtBearerOptions are somewhat similar to your example – should I use this call *instead* of app.UseIdentityServerAuthentication() as all QuickStarts indicate, or in addition to?

    Hopefully, the question makes sense.
    Thanks

    1. Hi Felix

      Yes the arcticle is up to date. I’m updating now to rc5. Both Authentication extension methods work. You can use which ever one you prefer or see better.

      Greetings Damien

  7. Hi, Have you tried this with Custom Policies. I did but for some reason my requirement handler is not called when i visit the url protected by the policy

    1. Nevermind I forgot to register services.AddSingleton();

  8. brian kim · · Reply

    i have little issue with InMemoryUser section? where should i have to put? i check src code..but i cant see any InMemoryUser code..

      1. brian kim · ·

        thanks. a lot .

  9. Hi Damien,
    Your tutorials are fantastic, they’re the most comprehensive as i am trying to understand in detail using identityserver.

    I have many questions regarding claims and scopes and i am hoping you could help.
    I am not sure i understand them properly and how they fit in each piece of the puzzle.

    You mentioned :
    “the claimType parameter in the RequireClaim method must match a claim supplied in the token.” but i couldn’t find the claimType “scope” mentioned at [ RequireClaim(“scope”, “dataEventRecords”) ].

    Also regarding the above statement, can you please explain why concept of claim type ends up being just another claim on the token ?

    When defining an ApiResource, the resource has a UserClaims property, as well as you can define scopes for the resource and each scope can have claims, what is the difference between those ?

    Are we defining here all those possible claims for this resource server ?

    Thanks,

  10. sirajmansour · · Reply

    Hi Damien,
    Your tutorials are fantastic, they’re the most comprehensive as i am trying to understand in detail using identityserver.

    I have many questions regarding claims and scopes and i am hoping you could help.
    I am not sure i understand them properly and how they fit in each piece of the puzzle.

    You mentioned :
    “the claimType parameter in the RequireClaim method must match a claim supplied in the token.” but i couldn’t find the claimType “scope” mentioned at [ RequireClaim(“scope”, “dataEventRecords”) ].

    Also regarding the above statement, can you please explain why concept of claim type ends up being just another claim on the token ?

    When defining an ApiResource, the resource has a UserClaims property, as well as you can define scopes for the resource and each scope can have claims, what is the difference between those ?

    Are we defining here all those possible claims for this resource server ?

    Thanks,

  11. mark baer · · Reply

    Hey Damien, nice article. Instead of In Memory users, do you have any examples on loading the User Roles/Claims from an Identity DB into the token when the user logs in? I’m not sure where or how that happens. Thanks

  12. Hi Damien
    I followed your guide lines and the latest code but could not make authorization work in “ResourceWithIdentityServerWithClient” project with
    [Authorize(AuthenticationSchemes = “Bearer”, Policy = “dataEventRecords”)] at class level
    and [Authorize(“dataEventRecordsUser”)] or similar at method level.
    It throws Status code 500 error.
    If I remove authorization attributes or simple use [Authorize] it works

    Any help in suggesting what is wrong in my approach?

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

%d bloggers like this: