Reputation: 13649
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
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