API Security with ASP.NET Core 5.0 and Azure AD for Dummies
This blog is part of a complete blog series.
- Part 1: Authentication vs authorization
- Part 2: The different actors
- Part 3: Authentication with Azure AD
- Part 4: Authorization with Access Control List (this blog)
- Part 5: Authorization with Application Roles
- Part 6: Authorization with Delegated Permissions
- Part 7: Retrieve more user information
- Part 8: Access APIs on behalf of a user (coming soon)
- Part 9: Automate the Azure AD configuration (coming soon)
API security metaphor
If we get back to the party metaphor, the following steps are performed:
- The ticket desk gives each authenticated user a badge.
- The bodyguard only allows people with a valid badge, that are on the guest list.
In the context of API security, it goes like this:
- Azure AD returns an access token (JWT) in return for a successful authentication
- The API has a global security check to see if the consuming client id (azp claim) is on the allowed list
An Access Control List is ideal for these security requirements:
- There is a limited number of applications that consumes the API (e.g. API is behind a gateway)
- The user context should not be considered at al
- The authorization must happen on the global API level
Implement the ACL on the API
Now it’s time to secure our API, by adding Access Control List authorization to the API.
From the previous part, you might remember that the daemon application could not successfully call the API. Instead, a 500 Internal Server Error was thrown with this following exception message: IDW10201: Neither scope or roles claim was found in the bearer token.
The reason for this exception, is the fact that there is a default validation that checks if the access token contains a scope or roles. In case of a daemon client, there is never a scope present, because the client credentials flow uses the /.default scope, which is not propagated in the access token. Roles are supported for daemon applications, this will be tackled in the next part. In our scenario, we just want a simple validation on the client id of the client application.
Luckily, we can override this default behavior and implement a ACL:
- Open the PartyApi solution from the `02-with-authentication` folder
- Add this setting to the AzureAd section of the appsettings.json file. This disables the scope/role validation.
"AllowWebApiToBeAuthorizedByACL": true
- Add the Access Control List to the AzureAd section of the appsettings.json file.
"AccessControlList": [ "<DAEMON-CLIENT-APP-CLIENT-ID>", "<USER-CLIENT-APP-CLIENT-ID>" ]
- Update the AuthorizationPolicyBuilder in the Startup.cs file, to check that the azp claim is part of the configured Access Control List
var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .RequireClaim("azp", Configuration.GetSection("AzureAd:AccessControlList").Get<string[]>()) .Build();
- From now on, calling the API will only succeed if the client application is allowed in the ACL
Conclusion
In this part, we have seen a simple, but effective way of implementing authorization via a configurable Access Control List. This is the perfect scenario when you have a very limited number of client applications and you do not have to take into account a potential user context. A good example is an API that sits behind an API gateway. It is often sufficient to add the API gateway to the Access Control List of the backend API.