Most Web applications use OAuth 2.0 to authenticate/authorize uses these days. ASP.NET also has built-in support for OAuth 2.0 with Azure AD which we use quite often.
However, in many projects, we found it difficult to include appropriate claims from the beginning because:
- Management team of AD is different
- There are strict policies that we cannot create test AD
- We don't know what scope to be created yet
In such care, we often neglect authorization settings in controller, but we shouldn't. To avoid this, I explain how to use middleware to modify passed claim so that we can tweak JWT.
There are several things to keep in our mind. (Great comment by @phlash909)
- If we modify JWT in this way, we cannot pass it to another service anymore.
- We can also offload this to another service, rather than directly modify claims to keep it clean.
Web API
In this article, I start from normal Web API using template.
dotnet new webapi -n modifyjwt
cd modifyjwt
start .\modifyjwt.csproj
Enable Authentication
Refer to Quickstart: Protect a web API with the Microsoft identity platform to enable authentication against your Azure AD.
Add require scope to existing controller
By default, there are not authorization settings in existing controller.
1. Install Microsoft.Identity.Web nuget package.
2. Add [RequiredScope]
attribute to WeatherForecastController.cs
. I added ReadWeatherForecast
as required scope.
[HttpGet(Name = "GetWeatherForecast")]
[RequiredScope("ReadWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
3. Run the application and confirm you cannot access the controller.
Add middleware to modify JWT
1. Add middleware class.
using System.Security.Claims;
namespace modifyjwt;
public class MyAuthMiddleware
{
private const string SCOPE_CLAIM_TYPE = "http://schemas.microsoft.com/identity/claims/scope";
private readonly RequestDelegate _next;
public MyAuthMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.User.Identity is not null
&& context.User.Identity.IsAuthenticated)
{
Claim scope = context.User.Claims.First(x => x.Type == SCOPE_CLAIM_TYPE);
(context.User.Identity as ClaimsIdentity)?.RemoveClaim(scope);
context.User.AddIdentity(new ClaimsIdentity(new List<Claim>()
{
new Claim(SCOPE_CLAIM_TYPE, "ReadWeatherForecast"),
}));
}
// Call the next delegate/middleware in the pipeline.
await _next(context);
}
}
public static class MyAuthMiddlewareExtensions
{
public static IApplicationBuilder UseMyAuthMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyAuthMiddleware>();
}
}
2. Use it in Program.cs between UseAuthentication
and UseAuthorization
app.UseAuthentication();
app.UseMyAuthMiddleware();
app.UseAuthorization();
3. Run the app to confirm it works.
Add multiple scopes
If we need to add multiple scopes, we can add it by separate each scope with space like ReadWeatherForecast WriteWeatherForecast
.
Summary
In production, we should get all necessary scopes from Azure AD, or at least fetch from data source by using user id.
Top comments (5)
Neat way of testing the remainder of the stack below the top level controller in a single-tier web application, but surely adjusting the claims in a token invalidates the signature? This will be a problem if working with a multi-service (eg: microservice) architecture that passes tokens between service calls (as it should), since those tokens will no longer pass integrity checks.
I'm surprised that there are policy / governance issues with a dev team operating their own local Azure AD for testing (especially given how easy it is!), this presents no risk to production (tokens would be signed by a different authority), and frees the team to try different authorization models, working with the business & operations teams as the stakeholders for how authentication/authorization will work in production, and the customer experience that will result.
Often, it pays to design from the business model (how products are sold) towards the detailed authorization checks that individual services are required to make, and work to reduce the coupling that this creates. In my last position we chose to isolate service-level authorization checks into a sidecar component that was deployed alongside services, connected by an API that took in the service ID, the user-ID token, the requested action and resource ID, returning a simple yes/no. This makes it mockable for local development, replaceable in different environments (CI, UAT, etc.) and critically for the dev team, somebody else's problem.
Can you tell that I used to look after the architecture of all this in my last position 😁
I totally agree with you that it should be easy and safe to create test Azure AD environment, which we want more customers to realize :). I also like your approach to use policy base service like OPA to take care of policy base authorization or any other components to handle it would be more maintainable way, which we also consider.
Think about business model is the key to design the service as it affects entire architecture for sure <3
By the way, are you using OPA or any other service for policy check?
Thanks for reading my brain dump, and mentioning the possible issues in the article ;)
I believe the platform team (who owned the customer authorization flows) were considering the use of OPA (Open Policy Agent for those following along!) when I retired... it seemed like overkill at the time, so an anti-corruption API as I described was defined to permit changes to the authorization sidecar after services had been deployed.
I should note that we supported both federated access from our customer's OpenID Connect identity providers^, and managed accounts in an identity provider we operated (as an additional paid service) should they not have a corporate system in place or wanted to isolate accounts from their corporate system.
^ as a business-to-business company, this covered the majority of customers who typically had Azure AD, AWS Cognito, Google auth, Auth0 or similar in place. The major benefit to them was local control of accounts/service access, and to us - no more helpdesk calls to reset credentials! Federation FTW 😁
That's exactly I think every day!! Not many customers use only one service provider. Especially B2C service should consider supporting multiple Ids from first place so that we don't have to change architecture later.
I will consider writing another blog later about OPA and Web API as well.
@phlash909 As your comment is important, I included the consideration in the article. Thanks.