Problems handling OnTokenValidated with a delegate assigned in startup.cs - c#

I want to properly use DI in ASP.NET Core 2.0 in order to have my custom method handle the OnTokenValidated event that fires after a JWT token is validated during authentication. The solution below works, except that in the handler I use an injected service that hits MemoryCache to check for cached items added elsewhere in a controller (I've verified that they're added and persisted), and when it's accessed, the cache is always empty. I suspect this is because my custom handler object is being created by a different container (due to the early BuildServiceProvider() call?) and is utilizing a separate instance of MemoryCache (or similar).
If that's the case, I guess I'm not clear on how to properly add and reference my class and method in ConfigureServices() in startup.cs so that it works as intended. Here's what I have:
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
...
services.AddScoped<IJwtTokenValidatedHandler, JwtTokenValidatedHandler>();
// add other services
...
var sp = services.BuildServiceProvider();
services.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, bOptions =>
{
// Configure JwtBearerOptions
bOptions.Events = new JwtBearerEvents
{
OnTokenValidated = sp.GetService<JwtTokenValidatedHandler>().JwtTokenValidated
};
}
My custom handler class is below. The ValidateSessionAsync() call uses an injected AppSessionService to access the MemoryCache object and ensure a cache entry exists:
public class JwtTokenValidatedHandler : IJwtTokenValidatedHandler
{
AppSessionService _session;
public JwtTokenValidatedHandler(AppSessionService session)
{
_session = session;
}
public async Task JwtTokenValidated(TokenValidatedContext context)
{
// Add the access_token as a claim, as we may actually need it
var accessToken = context.SecurityToken as JwtSecurityToken;
if (Guid.TryParse(accessToken.Id, out Guid sessionId))
{
if (await _session.ValidateSessionAsync(sessionId))
{
return;
}
}
throw new SecurityTokenValidationException("Session not valid for provided token.");
}
}
If the custom OnTokenValidated method contained simple logic and didn't need an injected service I would inline it with an anonymous function or declare it privately in startup.cs. I'd prefer to fix this approach if I can, but I'd be open to other ones.

Instead of using static/singleton events, consider subclassing JwtBearerEvents and using the JwtBearerOptions.EventsType option:
public class CustomJwtBearerEvents : JwtBearerEvents
{
AppSessionService _session;
public CustomJwtBearerEvents(AppSessionService session)
{
_session = session;
}
public override async Task TokenValidated(TokenValidatedContext context)
{
// Add the access_token as a claim, as we may actually need it
var accessToken = context.SecurityToken as JwtSecurityToken;
if (Guid.TryParse(accessToken.Id, out Guid sessionId))
{
if (await _session.ValidateSessionAsync(sessionId))
{
return;
}
}
throw new SecurityTokenValidationException("Session not valid for provided token.");
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<CustomJwtBearerEvents>();
services.AddAuthentication()
.AddJwtBearer(options =>
{
options.EventsType = typeof(CustomJwtBearerEvents);
});
}
}

Related

OAuthHandler.Options property is null?

defined an oauth handler like so which works just fine.
public class MyHandler : OAuthHandler<MyOptions> {
public MyHandler(IOptionsMonitor<MyOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock) { }
// overriden CreateTicketAsync and BuildChallengeUrl protected methods.
}
but the authenticator requires that user can revoke authorization which doesn't seem to be supported by oauth in a native fashion. so in the handler, I added a specialization which is to revoke authorization like so.
public class MyHandler : OAuthHandler<MyOptions> {
public async Task RevokeAuthorizationAsync(string token) {
if(token == null) throw new ArgumentNullException(nameof(token));
// below, Options throws a NullReferenceException???
var request = new HttpRequestMessage(HttpMethod.Post, Options.RevocationEndpoint);
// other relevant code here...
}
}
so when the user hits the revoke button from the app, the account controller revoke method is called.
public class AccountController : Controller {
[Authorize]
public async Task Revoke()
=> HttpContext.RevokeAuthorizationAsync("refreshTokenObtainedByWhateverMean");
}
// which in turn calls upon the HttpContext extension written expressly for the purpose
public static class MyHttpContextExtensions {
public static async Task RevokeAuthorizationAsync(this HttpContext context, string accessOrRefreshToken) {
var handler=context.RequestServices.GetRequiredService<MyHandler>();
await handler.RevokeAuthorizationAsync(accessOrRefreshToken);
}
}
upon the call to MyHandler.RevokeAuthorizationAsync, the OAuthHandler.Options is null? how can that be? when authenticating and authorizing from within the MyHandler.CreateTicketAsync and MyHandler.BuildChallengeUrl, the OAuthHandler.Options property is set.
I suspect that I might not instantiate the MyHandler class properly using the HttpContext.RequestServices.GetRequiredService method. but if this is not it, how could I specialize MyHandler and provide OAuthHandler.Options ? because I need the Options.ClientId and Options.ClientSecret to revoke the authorization. both are configured like so from the program class.
public class Program {
builder.Services
.AddAuthentication(o => {
// some config here
})
.AddCookie()
.AddMyAuthenticator(o => {
o.ClientId=Configuration["ClientId"];
o.ClientSecret=Configuration["ClientSecret"];
});
}
so how is that OAuthHandler.Options is instantiated within the MyHandler.CreateTicketAsync and MyHandler.BuildChallengeUrl, and is null when I call upon the MyHandler.RevokeAuthorizationAsync method?

Get a service from the builder.Services.AddAuthentication() method

I want to get a registered service from within the AddAuthentication() method but I cannot do so without re-registering all the services again (in BuildServiceProvider).
I get the warning:
"Calling buildserviceprovider from application code results in an additional copy of services."
Is there a way to pass in IServiceCollection? It seems odd it is not already available seeing as I have access to "builder.Services".
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var context = builder.Services.BuildServiceProvider().GetService<IHttpContextAccessor>();
//I want to do this but it's not available.:
options.GetService<IHttpContextAccessor>();
//OR
builder.Services.GetService<IHttpContextAccessor>();
}
First implement IConfigureNamedOptions
public class ConfigurationsJwtBearerOptions : IConfigureNamedOptions<ConfigurationsJwtBearerOptions>
{
IHttpContextAccessor _httpContext;
public ConfigurationsJwtBearerOptions(IHttpContextAccessor httpContext)
{
_httpContext = httpContext;
}
public void Configure(string name, ConfigurationsJwtBearerOptions options)
{
Configure(options);
}
public void Configure(ConfigurationsJwtBearerOptions options)
{
//same code that you usually used in AddJwtBearer (options=>{})
}
}
Then in Progam.cs or StarUp.cs
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.ConfigureOptions<ConfigurationsJwtBearerOptions>().AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();//no need to configurate JwtBearer options here ConfigurationsJwtBearerOptions will handle it

.NET Core 6: Use my DbContext in Authorization Handler

I have an authorization handler that needs to pull data from the database to complete the authorization logic. The idea is that users are only allowed to certain areas after posting a given number of blog posts.
Code as follows:
namespace MyProject.Authorisation
{
public class MinimumPostRequirement : IAuthorizationRequirement
{
public MinimumPostRequirement (int postCount)
{
PostCount = postCount;
}
public int PostCount { get; }
}
public class MinimumPostRequirement Handler : AuthorizationHandler<MinimumPostRequirement >
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext authContext, ApprovedUserRequirement requirement)
{
using (MyDbContext _context = new MyDbContext())
{
int? postCount = _context.Posts.Where(post => post.UserName == authContext.User.Identity.Name).Count();
if(postCount == null)
{
return Task.CompletedTask;
}
if(postCount >= requirement.PostCount)
{
authContext.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
}
Here is how I declare it in Program.cs:
//DB Connection
var connectionString = builder.Configuration.GetConnectionString("MyConnection");
builder.Services.AddDbContext<MyDbContext>(options => options.UseSqlServer(MyConnection));
//authorisation
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireMinimumPosts", policy => policy.Requirements.Add(new MinimumPostRequirement(3)));
});
builder.Services.AddSingleton<IAuthorizationHandler, MinimumPostRequirementHandler>();
I know there is a dependency injection issue when adding a singleton, so I have also tried using in Program.cs:
builder.Services.AddScoped<IAuthorizationHandler, MinimumPostRequirementHandler>();
And
builder.Services.AddTransient<IAuthorizationHandler, MinimumPostRequirementHandler>();
All result in the following error:
InvalidOperationException: No database provider has been configured for this DbContext. A provider can be configured by overriding the 'DbContext.OnConfiguring' method or by using 'AddDbContext' on the application service provider. If 'AddDbContext' is used, then also ensure that your DbContext type accepts a DbContextOptions object in its constructor and passes it to the base constructor for DbContext.
The database works for all other site operations. The problem only arises when I add [Authorize(Policy = "RequireMinimumPosts")] to the methods I want to restrict.
How would you write this code so that it works? How does dependency injection work in this context? Is there anything I am missing?
Inject the DbContext into the constructor of your MinimumPostRequirementHandler so it will be resolved by the DI container.
public class MinimumPostRequirementHandler
: AuthorizationHandler<MinimumPostRequirement>
{
private readonly MyDbContext _dbContext;
public MinimumPostRequirementHandler( MyDbContext dbContext )
{
_dbContext = dbContext;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext authContext, ApprovedUserRequirement requirement)
{
// use _dbContext here
}
}
Register the handler as scoped.

Dependency injection in authentication options

I'm trying to inject a service into a ValidationHandler that inherits from JwtSecurityTokenHandler which validates the Jwt's signature. Unfortunately, to use the handler, I have to use object initialization with new in ConfigureServices, which means I can't use the injected services that comes with adding the service to the dependency container.
public class DynamicKeyJwtValidationHandler : JwtSecurityTokenHandler
{
private readonly IMemoryCache _cache;
public DynamicKeyJwtValidationHandler(IMemoryCache cache)
{
_cache = cache;
}
}
services.AddTransient<DynamicKeyJwtValidationHandler>();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opts =>
{
opts.SecurityTokenValidators.Clear();
opts.SecurityTokenValidators.Add(new DynamicKeyJwtValidationHandler(???));
});
So what can I do to still be able to use the IMemoryCache?
You can create an implementation of IConfigureNamedOptions<JwtBearerOptions>:
public class JwtOptionsConfigurer : IConfigureNamedOptions<JwtBearerOptions>
{
private readonly DynamicKeyJwtValidationHandler _tokenValidator;
public JwtOptionsConfigurer(DynamicKeyJwtValidationHandler tokenValidator)
{
_tokenValidator = tokenValidator;
}
public void Configure(string name, JwtBearerOptions options)
{
options.SecurityTokenValidators.Clear();
options.SecurityTokenValidators.Add(_tokenValidator);
}
public void Configure(JwtBearerOptions options)
{
Configure(JwtBearerDefaults.AuthenticationScheme, options);
}
}
And then add it like so:
services.AddSingleton<IConfigureOptions<JwtBearerOptions>, JwtOptionsConfigurer>();
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();
We still need to call .AddJwtBearer() because that does some necessary registrations, etc.
Side note (in case it's useful to anyone): the authentication middleware creates a new JwtBearerOptions every time it is needed, so the configuration code above will be run multiple times.

Best way to inject instance of DynamoDBContext in .NET Core

Currently working on a web services project for my class and have decided to make a web API using .NET Core and DynamodDB.
I was just curious what the best way to inject the DynamoDBContext is?
I currently am doing it like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDefaultAWSOptions(Configuration.GetAWSOptions());
services.AddAWSService<IAmazonDynamoDB>();
}
I got this piece of code above from the DynamoDB documentation. I add an instance of IAmazonDynamoDB to the project.
DynamoDBContext context;
public ValuesController(IAmazonDynamoDB context)
{
this.context = new DynamoDBContext(context);
}
In the controller, I then inject the IAmazonDynamoDB instance, and use that to create an instance of DynamoDBContext.
Is there a way to create an instance of the context in the ConfigureServices method and add it to the project there, or is the way I am doing it currently fine?
Is there a way to create an instance of the context in the
ConfigureServices method and add it to the project there, or is the
way I am doing it currently fine?
Although your solution will work, it has a drawback. You're not using Dependency Injection for DynamoDBContext and create its instance in controller constructor through new operator. You'll face a problems when it comes to unit testing your code, because you have no way to substitute implementation of DynamoDBContext.
The proper way is to register DynamoDBContext in DI container and let the container itself create an instance when it's required. With such approach IDynamoDBContext gets injected into ValuesController:
public class ValuesController
{
private readonly IDynamoDBContext context;
public ValuesController(IDynamoDBContext context)
{
this.context = context;
}
// ...
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDefaultAWSOptions(Configuration.GetAWSOptions());
services.AddAWSService<IAmazonDynamoDB>();
services.AddTransient<IDynamoDBContext, DynamoDBContext>();
}
Basically you'd have to create an interface for your DynamoDB context
public interface IDynamoDbContext<T> : IDisposable where T : class
{
Task<T> GetByIdAsync(string id);
Task SaveAsync(T item);
Task DeleteByIdAsync(T item);
}
Create a class implementing the interface
public class DynamoDbContext<T> : DynamoDBContext, IDynamoDbContext<T>
where T : class
{
public DynamoDbContext(IAmazonDynamoDB client)
: base(client)
{
}
public async Task<T> GetByIdAsync(string id)
{
return await base.LoadAsync<T>(id);
}
public async Task SaveAsync(T item)
{
await base.SaveAsync(item);
}
public async Task DeleteByIdAsync(T item)
{
await base.DeleteAsync(item);
}
}
Inject it in your Startup like this
var client = Configuration.GetAWSOptions().CreateServiceClient<IAmazonDynamoDB>();
services.AddScoped<IDynamoDbContext<AwesomeClass>>(provider => new DynamoDbContext<AwesomeClass>(client));
The context will be passed in the DI system and you can use it where you like
private IDynamoDbContext<AwesomeClass> _awesomeContext;
public AwesomeDynamoDbService(IDynamoDbContext<AwesomeClass> awesomeContext)
{
_awesomeContext= awesomeContext;
}
I faced a similar issue and wrote a blog post describing a good way to fix it! Hope it shares some light!
This is how I am using it to make it work with both local & public (AWS hosted) DynamoDB.
appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AWS": {
"Profile": "default",
"Region": "ap-south-1"
},
"DynamoDb": {
"LocalMode": false,
"LocalServiceUrl": "http://localhost:8001",
"TableNamePrefix": ""
}
}
Program.cs in .NET 6
// Get the AWS profile information from configuration providers
AWSOptions awsOptions = builder.Configuration.GetAWSOptions();
// Configure AWS service clients to use these credentials
builder.Services.AddDefaultAWSOptions(awsOptions);
var dynamoDbConfig = builder.Configuration.GetSection("DynamoDb");
var runLocalDynamoDb = dynamoDbConfig.GetValue<bool>("LocalMode");
#region DynamoDB setup
if (runLocalDynamoDb)
{
builder.Services.AddSingleton<IAmazonDynamoDB>(sp =>
{
var clientConfig = new AmazonDynamoDBConfig { ServiceURL = dynamoDbConfig.GetValue<string>("LocalServiceUrl") };
return new AmazonDynamoDBClient(clientConfig);
});
}
else
{
builder.Services.AddAWSService<IAmazonDynamoDB>();
}
builder.Services.AddSingleton<IDynamoDBContext, DynamoDBContext>((serviceProvider) =>
{
IAmazonDynamoDB amazonDynamoDBClient = serviceProvider.GetRequiredService<IAmazonDynamoDB>();
DynamoDBContextConfig dynamoDBContextConfig = new DynamoDBContextConfig
{
TableNamePrefix = dynamoDbConfig.GetValue<string>("TableNamePrefix")
};
return new DynamoDBContext(amazonDynamoDBClient, dynamoDBContextConfig);
});
#endregion
If you are using Lambda then you can try ti use below code
In your DynamoDB database class add constructor with dependency on IDynamoDBContext
public DynamoDbDatabase(IDynamoDBContext dynamoDbContext)
{
_dynamoDbContext = dynamoDbContext;
}
In Function.cs of your Lambda define mapping for dependency injection
private static IServiceProvider ConfigureServices()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IDynamoDBContext,DynamoDBContext>(p => new DynamoDBContext(new AmazonDynamoDBClient()));
return serviceCollection.BuildServiceProvider();
}
Call above function on top of your lambda function handler
when your code runs it will automatically detect dependency and pass proper DynamoDBContext object when asked for IDynamoDBContext

Categories