yonan2236
yonan2236

Reputation: 13649

Dependency Injection - There is no argument given that corresponds to the required parameter

I found a couple of sample code implementations of JWT authentication on the internet but I'm having difficulties in making these codes work together (since I got these codes from various sources).

I'm trying to add JWT authentication to the generated WeatherForecast API project (Web API) in visual studio. Below are the changes I made to the default project:

1. Create a JWT Service class: Create a new class that will handle creating and validating JWTs. This class should have methods for creating a JWT given a set of claims, and for validating a JWT and returning the claims contained within it.

public class JwtService
{
    private readonly SymmetricSecurityKey _signingKey;

    public JwtService(string secretKey)
    {
        _signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
    }

    public string CreateToken(ClaimsIdentity claims)
    {
        var jwt = new JwtSecurityToken(
            issuer: "app",
            audience: "app",
            claims: claims.Claims,
            notBefore: DateTime.Now,
            expires: DateTime.Now.AddDays(7),
            signingCredentials: new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256)
        );

        return new JwtSecurityTokenHandler().WriteToken(jwt);
    }

    public ClaimsPrincipal GetPrincipalFromToken(string token)
    {
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "app",
            ValidateAudience = true,
            ValidAudience = "app",
            ValidateLifetime = true,
            IssuerSigningKey = _signingKey
        };

        var handler = new JwtSecurityTokenHandler();

        try
        {
            var principal = handler.ValidateToken(token, tokenValidationParameters, out var validatedToken);
            return principal;
        }
        catch (SecurityTokenValidationException)
        {
            return null;
        }
    }
}

2. Create a JWT Authentication Service class: Create a new class that will handle authenticating users and returning a JWT. This class should have a method for authenticating a user given a username and password and returning a JWT if the authentication is successful.

public class JwtAuthenticationService
{
    private readonly JwtService _jwtService;

    public JwtAuthenticationService(JwtService jwtService)
    {
        _jwtService = jwtService;
    }

    public string Authenticate(string username, string password)
    {
        // authenticate user
        var authenticated = username == "test" && password == "test";

        if (!authenticated)
            return null;

        // create claims
        var claims = new ClaimsIdentity(new[]
        {
                new Claim(ClaimTypes.Name, username),
                new Claim(ClaimTypes.Role, "User")
            });

        // create token
        return _jwtService.CreateToken(claims);
    }
}

3. Create a JWT Authorization filter: Create a new class that will handle checking if the request contains a valid JWT and, if it does, adding the claims contained within the JWT to the current user's claims.

public class JwtAuthorizationFilter : AuthorizationFilterAttribute
{
    private readonly JwtService _jwtService;

    public JwtAuthorizationFilter(JwtService jwtService)
    {
        _jwtService = jwtService;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        // check if the request contains an Authorization header
        var authHeader = context.HttpContext.Request.Headers["Authorization"].FirstOrDefault();

        if (authHeader == null || !authHeader.StartsWith("Bearer "))
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        // get the token from the header
        var token = authHeader.Substring("Bearer ".Length).Trim();
        // validate the token and get the claims
        var claimsPrincipal = _jwtService.GetPrincipalFromToken(token);

        if (claimsPrincipal == null)
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        // set the current user's claims
        var identity = (ClaimsIdentity)claimsPrincipal.Identity;
        var claims = identity.Claims.ToList();
        var genericIdentity = new GenericIdentity(identity.Name);

        genericIdentity.AddClaims(claims);

        var genericPrincipal = new GenericPrincipal(genericIdentity, new string[] { identity.RoleClaimType });

        context.HttpContext.User = genericPrincipal;
    }
}

4. Add the filter to the WeatherForecastController: Apply the filter to the WeatherForecastController class using the [JwtAuthorizationFilter] attribute.

[ApiController]
[Route("api/[controller]")]
[JwtAuthorizationFilter]
public class WeatherForecastController : ControllerBase
{
    private readonly IWeatherForecastService _weatherForecastService;
    
    public WeatherForecastController(IWeatherForecastService weatherForecastService)
    {
        _weatherForecastService = weatherForecastService;
    }
    
    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        // check if the user is authorized
        if (!User.IsInRole("User"))
        {
            throw new UnauthorizedAccessException();
        }

        // return the weather forecast data
        return _weatherForecastService.GetWeatherForecasts();
    }
}

5. Add the JWT service and authentication service to the Dependency Injection container: In the Startup.cs class, add the JwtService and JwtAuthenticationService classes to the Dependency Injection container so that they can be injected into other classes as dependencies.**

services.AddSingleton<JwtService>(new JwtService("your_secret_key"));
services.AddSingleton<JwtAuthenticationService>();

6. Add the JWT filter to the Dependency Injection container: In the Startup.cs class, add the JwtAuthorizationFilter to the Dependency Injection container so that it can be applied to controllers as a filter.**

services.AddSingleton<JwtAuthorizationFilter>();

Here's the final Startup.cs after the changes:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<JwtService>(new JwtService("your_secret_key"));
        services.AddSingleton<JwtAuthenticationService>();
        services.AddSingleton<JwtAuthorizationFilter>();
        services.AddControllers();
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo { Title = "WeatherForecast", Version = "v1" });
        });
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseSwagger();
            app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WeatherForecast v1"));
        }

        app.UseHttpsRedirection();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

The Error:

There is no argument given that corresponds to the required parameter 'jwtService' of 'JwtAuthorizationFilter.JwtAuthorizationFilter(JwtService)'

The error is encountered at the line where I added the [JwtAuthorizationFilter] attribute on the WeatherForecastController class:

[ApiController]
[Route("[controller]")]
[JwtAuthorizationFilter] // <-- Error occurs at this line
public class WeatherForecastController : ControllerBase { ...

I understand that JwtAuthorizationFilter has a constructor that accepts an argument of JwtService type but I'm not sure how exactly to supply it in the controller or in the Startup.cs.

Upvotes: 0

Views: 481

Answers (1)

DavidG
DavidG

Reputation: 118957

You can't use constructor DI like this for an attribute. Instead, get the services you require in inside the attribute. For example:

public class JwtAuthorizationFilter : AuthorizationFilterAttribute
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var jwtService = context.HttpContext.RequestServices
            .GetRequiredService<JwtService>();

        //etc
    }
}

Upvotes: 1

Related Questions