0

The project that I'm working is running on .NET 6. The ASP.NET Core 6 Web API itself is not generating the JWT, but sending user creds to another authentication API and retrieves the bearer token from there.

My goal is: when user requests an authorized action, the API should take bearer token from header and send it to another API for validation and at the same time take userId from token and validate the user role with the database.

Currently I got to the point where created simple custom attribute and struggle with moving forward. Current state:

Programs.cs:

...
//app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

Created simple custom attribute:

public class MyAuthorize : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext httpContext)
    {   
       var token = httpContext.HttpContext.Request.Headers.Authorization.ToString().Replace("Bearer ", "");
       var jwt = new JwtSecurityTokenHandler().ReadJwtToken(token);
       var user = jwt.Claims.First(c => c.Type == "NameIdentifier")?.Value;
   }
}

And successfully applied custom attribute to a controller method:

[ApiController]
[Route("api/myapi")]
public class MyController: ControllerBase
{
    [HttpGet("test-me")]
    [MyAuthorize]
    public async Task<IActionResult> TestMe()
    {
        // do something
        return Ok();
    }
}

In the current state I can't find a way to inject services into MyAttribute to create custom logic for token and role validation.

My goal is to achieve something like this:

[HttpGet("test-me")]
[MyAuthorize(Roles="Admin")]
public async Task<IActionResult> TestMe()
{
    // do something
    return Ok();
}

Where MyAttribute will send http request to another API to validate the token and execute request to DB and validate user role.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Jivopis
  • 121
  • 11
  • "send it to another api for validation" - why not verify it within your own API? If you really need this you can add a [custom policy](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/iauthorizationpolicyprovider?view=aspnetcore-7.0) and inject a service into the constructor of the policy provider. [You don't want to inject into attributes](https://stackoverflow.com/a/29916075/3034273) – Xerillio Aug 07 '23 at 20:19
  • It means that I need to share secret between 2 Apis that is not good practice. – Jivopis Aug 07 '23 at 20:49
  • No. That depends on how you generate the signature. If you use an asymmetric signing algorithm, your "token API" keeps the private key, and your other API verifies with the public key. – Xerillio Aug 07 '23 at 21:00
  • Have you checked the token claims? Does that containing user credential? If token has the correct claims I think you can then check within the database based on the claims to match the role. – Md Farid Uddin Kiron Aug 08 '23 at 07:28
  • I'd found the solution. I made handlers for authentication and authorization. Will post the result soon. – Jivopis Aug 10 '23 at 14:27

1 Answers1

0

So I achived the result I want with the following changes.

Custom authentiation handler:

public class MyAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public MyAuthenticationHandler (
        IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock)
    : base(options, logger, encoder, clock)
    {
        // inject services here
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // Read token from HTTP request header
        string authorizationHeader = Request.Headers["Authorization"]!;
        if (string.IsNullOrEmpty(authorizationHeader) || !authorizationHeader.StartsWith("Bearer "))
        {
            return AuthenticateResult.Fail("no token");
        }
        // Remove "Bearer" to get pure token data
        var token = authorizationHeader.Substring("Bearer ".Length);

        try
        {
            //auth logic
            
            return AuthenticateResult.Success(ticket);
        }
        catch (Exception ex)
        {
            //oops
            return AuthenticateResult.Fail(ex);
        }            
    }
}

Custom authorization handler:

public class MyAuthorizationHandler : AuthorizationHandler<MyRequirement>, IAuthorizationRequirement
{

    public MyAuthorizationHandler()
    {
        // inject services here
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyRequirement requirement)
    {
        try
        {
            // do logic
            context.Succeed(requirement);
            return;
            
        }
        catch (Exception ex)
        {
            // handle error
        }
        
        context.Fail();
        return;
    }
}

Program.cs

...
builder.Services.AddScoped<IAuthorizationHandler, MyAuthorizationHandler>();
...
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddScheme<AuthenticationSchemeOptions, MyAuthenticationHandler>("Bearer", options => { });
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy(MyRolesPolicy.User, policy => policy.Requirements.Add(new MyRequirement("User")));
    options.AddPolicy(MyRolesPolicy.Admin, policy => policy.Requirements.Add(new MyRequirement("Admin")));
});
...
app.UseAuthentication();
app.UseAuthorization();
...

As result I can apply Authorize attribute to my controllers and actions:

[Authorize(MyRolesPolicy.User)]
public class MyController : BaseController
{
    ...
}
Jivopis
  • 121
  • 11