Setting dynamically OpenIdConnectOptions with AddOpenIdConnect overload that requires the AuthenticationScheme as well - asp.net-core

I have a question related to a green ticked solution from an old question:
how-can-i-set-the-authority-on-openidconnect-middleware-options-dynamically
I copy it here:
-- START COPY --
While a bit tricky, it's definitely possible. Here's a simplified example, using the MSFT OIDC handler, a custom monitor and path-based tenant resolution:
Implement your tenant resolution logic. E.g:
public class TenantProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
public TenantProvider(IHttpContextAccessor httpContextAccessor)
=> _httpContextAccessor = httpContextAccessor;
public string GetCurrentTenant()
{
// This sample uses the path base as the tenant.
// You can replace that by your own logic.
string tenant = _httpContextAccessor.HttpContext.Request.PathBase;
if (string.IsNullOrEmpty(tenant))
{
tenant = "default";
}
return tenant;
}
}
public void Configure(IApplicationBuilder app)
{
app.Use(next => context =>
{
// This snippet uses a hardcoded resolution logic.
// In a real world app, you'd want to customize that.
if (context.Request.Path.StartsWithSegments("/fabrikam", out PathString path))
{
context.Request.PathBase = "/fabrikam";
context.Request.Path = path;
}
return next(context);
});
app.UseAuthentication();
app.UseMvc();
}
Implement a custom IOptionsMonitor<OpenIdConnectOptions>:
public class OpenIdConnectOptionsProvider : IOptionsMonitor<OpenIdConnectOptions>
{
private readonly ConcurrentDictionary<(string name, string tenant), Lazy<OpenIdConnectOptions>> _cache;
private readonly IOptionsFactory<OpenIdConnectOptions> _optionsFactory;
private readonly TenantProvider _tenantProvider;
public OpenIdConnectOptionsProvider(
IOptionsFactory<OpenIdConnectOptions> optionsFactory,
TenantProvider tenantProvider)
{
_cache = new ConcurrentDictionary<(string, string), Lazy<OpenIdConnectOptions>>();
_optionsFactory = optionsFactory;
_tenantProvider = tenantProvider;
}
public OpenIdConnectOptions CurrentValue => Get(Options.DefaultName);
public OpenIdConnectOptions Get(string name)
{
var tenant = _tenantProvider.GetCurrentTenant();
Lazy<OpenIdConnectOptions> Create() => new Lazy<OpenIdConnectOptions>(() => _optionsFactory.Create(name));
return _cache.GetOrAdd((name, tenant), _ => Create()).Value;
}
public IDisposable OnChange(Action<OpenIdConnectOptions, string> listener) => null;
}
Implement a custom IConfigureNamedOptions<OpenIdConnectOptions>:
public class OpenIdConnectOptionsInitializer : IConfigureNamedOptions<OpenIdConnectOptions>
{
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly TenantProvider _tenantProvider;
public OpenIdConnectOptionsInitializer(
IDataProtectionProvider dataProtectionProvider,
TenantProvider tenantProvider)
{
_dataProtectionProvider = dataProtectionProvider;
_tenantProvider = tenantProvider;
}
public void Configure(string name, OpenIdConnectOptions options)
{
if (!string.Equals(name, OpenIdConnectDefaults.AuthenticationScheme, StringComparison.Ordinal))
{
return;
}
var tenant = _tenantProvider.GetCurrentTenant();
// Create a tenant-specific data protection provider to ensure
// encrypted states can't be read/decrypted by the other tenants.
options.DataProtectionProvider = _dataProtectionProvider.CreateProtector(tenant);
// Other tenant-specific options like options.Authority can be registered here.
}
public void Configure(OpenIdConnectOptions options)
=> Debug.Fail("This infrastructure method shouldn't be called.");
}
Register the services in your DI container:
public void ConfigureServices(IServiceCollection services)
{
// ...
// Register the OpenID Connect handler.
services.AddAuthentication()
.AddOpenIdConnect();
services.AddSingleton<TenantProvider>();
services.AddSingleton<IOptionsMonitor<OpenIdConnectOptions>, OpenIdConnectOptionsProvider>();
services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, OpenIdConnectOptionsInitializer>();
}
-- END COPY --
This works fine for me except that I would like to use another overload of the AddOpenIdConnect method, exactly the following one:
public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OpenIdConnectOptions> configureOptions);
My solution is:
public void ConfigureServices(IServiceCollection services)
{
// ...
// Register the OpenID Connect handler.
services.AddAuthentication()
.AddOpenIdConnect("MyExternalProvider", "My External Provider", options =>
{
var sp = services.BuildServiceProvider();
var initializer= sp.GetService<IConfigureNamedOptions<OpenIdConnectOptions>>();
initializer.Configure("MyExternalProvider",options); ;
}
);
services.AddSingleton<TenantProvider>();
services.AddSingleton<IOptionsMonitor<OpenIdConnectOptions>, OpenIdConnectOptionsProvider>();
services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, OpenIdConnectOptionsInitializer>();
}
My question is about if it is a good way to do it, building the Service Provider in the ConfigureServices method, to be used in a delegate?

Related

How to convert ODataQueryOptions<DtoType> to ODataQueryOptions<EntityType> to query the underlying storage?

I have a webapi controller using ProductDTO type for clients but the repository is using a Product type.
I would like to use odata on my endpoint. I receive the ODataQueryOptions parameter and I want to pass it to repository (implemented using CosmosDB).
I cant seem to figure out how to convert from ODataQueryOptions<ProductDTO> to ODataQueryOptions<Product>.
[Route("api/[controller]")]
public class ProductsController<ProductsDTO, Product> : ControllerBase
{
IRepository<Product> _repository;
IMapper _mapper;
[HttpGet]
public async Task<ActionResult<IList<ProductDTO>>> Get(ODataQueryOptions<ProductDTO> queryOptions)
{
var mappedQueryOptions = ... // convert 'queryOptions' to ODataQueryOptions<Product> ???
var products = await _repository.Get(mappedQueryOptions);
return Ok(_mapper.Map<IEnumerable<Product>, IEnumerable<ProductDTO>>(products));
}
}
In my aspnetcore service composition I create and inject automapper
var configuration = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new ProductProfile());
cfg.AddExpressionMapping();
});
internal class ProductProfile : Profile
{
public ProductProfile()
{
CreateMap<Product, ProductDto>().ReverseMap();
}
}
I managed to extract the queryoptions filter as a lambda expression Expression<Func<Product, bool>> (using automapper MapExpression) and passed it to repository , that works to a certain extent but I want to get the select , top, skip, etc. as well.
Any suggestions on how that could be done?
can you review the example?
[ODataRouteComponent("api")]
public class UsersController : ODataController
{
#region ctor
private readonly ILogger<UsersController> _logger;
private readonly IMapper _mapper;
private readonly UserManager<AspNetUser> _userManager;
public UsersController(IMapper mapper,
UserManager<AspNetUser> userManager,
ILogger<UsersController> logger)
{
_mapper = mapper;
_userManager = userManager;
_logger = logger;
}
#endregion
[HttpGet]
[EnableQuery(PageSize = 10)]
public async Task<IActionResult> Get(ODataQueryOptions<UserDto> options)
{
var user = await _userManager.GetUserAsync(User);
_logger.LogInformation("{UserUserName} is getting all users", user.UserName);
return Ok(await _userManager.Users.GetAsync(_mapper, options));
}
[HttpGet]
[EnableQuery]
public async Task<IActionResult> Get(int key, ODataQueryOptions<UserDto> options)
{
var user = await _userManager.GetUserAsync(User);
_logger.LogInformation("{UserUserName} is getting user with id {Key}", user.UserName, key.ToString());
return Ok(_userManager.Users.Get(_mapper, options).FirstOrDefault(x => x.Id == key));
}
}
services.AddControllers(opt =>{}).AddOData((opt, _) =>
{
opt.EnableQueryFeatures().AddRouteComponents("api", EdmModelBuilder.GetEdmModelv1());
});
public class MappingProfile : Profile
{
public MappingProfile() : base(nameof(WebUI))
{
CreateMap<AspNetUser, UserDto>();
}
}
internal static class EdmModelBuilder
{
internal static IEdmModel GetEdmModelv1()
{
ODataConventionModelBuilder builder = new();
builder.EnableLowerCamelCase();
#region UserDto
builder.EntitySet<UserDto>(nameof(DataListContext.Users))
.EntityType
.Page(25, 15);
var f5 = builder.Function("Get");
f5.Parameter<int>("key").Required();
f5.ReturnsFromEntitySet<UserDto>(nameof(DataListContext.Users));
#endregion
return builder.GetEdmModel();
}
}

How to create a ApiKey authentication scheme for .NET Core

I trying to set ApiKey authentication scheme to my Api but can't find any post or documentation about it.
Closer thing i found is this Microsoft page, but nothing say on how to register a authentication handler.
I'm using .Net Core 6.
Found out a solution mostly based on this great Joonas Westlin guide to implement basic authentication scheme. Credits should go to him.
Steps:
1. Implement the options class inheriting from `AuthenticationSchemeOptions` and other boiler classes that will be need after.
2. Create the handler, inherit from `AuthenticationHandler<TOptions>`
3. Override handler methods `HandleAuthenticateAsync` to get the key and call your implementation of `IApiKeyAuthenticationService`
4. Register the scheme with `AddScheme<TOptions, THandler>(string, Action<TOptions>)` on the `AuthenticationBuilder`, which you get by calling `AddAuthentication` on the service collection
5. Implement the `IApiKeyAuthenticationService` and add it to Service Collection.
Here all the code.
The AuthenticationSchemeOptions and other boiler classes:
//the Service interface for the service that will get the key to validate against some store
public interface IApiKeyAuthenticationService
{
Task<bool> IsValidAsync(string apiKey);
}
//the class for defaults following the similar to .Net Core JwtBearerDefaults class
public static class ApiKeyAuthenticationDefaults
{
public const string AuthenticationScheme = "ApiKey";
}
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions{}; //Nothing to do
public class ApiKeyAuthenticationPostConfigureOptions : IPostConfigureOptions<ApiKeyAuthenticationOptions>
{
public void PostConfigure(string name, ApiKeyAuthenticationOptions options){} //Nothing to do
};
The handler:
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private const string AuthorizationHeaderName = "Authorization";
private const string ApiKeySchemeName = ApiKeyAuthenticationDefaults.AuthenticationScheme;
private readonly IApiKeyAuthenticationService _authenticationService;
public ApiKeyAuthenticationHandler(
IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IApiKeyAuthenticationService authenticationService)
: base(options, logger, encoder, clock)
{
_authenticationService = authenticationService;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey(AuthorizationHeaderName))
{
//Authorization header not in request
return AuthenticateResult.NoResult();
}
if (!AuthenticationHeaderValue.TryParse(Request.Headers[AuthorizationHeaderName], out AuthenticationHeaderValue? headerValue))
{
//Invalid Authorization header
return AuthenticateResult.NoResult();
}
if (!ApiKeySchemeName.Equals(headerValue.Scheme, StringComparison.OrdinalIgnoreCase))
{
//Not ApiKey authentication header
return AuthenticateResult.NoResult();
}
if ( headerValue.Parameter is null)
{
//Missing key
return AuthenticateResult.Fail("Missing apiKey");
}
bool isValid = await _authenticationService.IsValidAsync(headerValue.Parameter);
if (!isValid)
{
return AuthenticateResult.Fail("Invalid apiKey");
}
var claims = new[] { new Claim(ClaimTypes.Name, "Service") };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.Headers["WWW-Authenticate"] = $"ApiKey \", charset=\"UTF-8\"";
await base.HandleChallengeAsync(properties);
}
}
The extensions to the AuthenticationBuilder class to make it easy to register the ApiKey scheme:
public static class ApiKeyAuthenticationExtensions
{
public static AuthenticationBuilder AddApiKey<TAuthService>(this AuthenticationBuilder builder)
where TAuthService : class, IApiKeyAuthenticationService
{
return AddApiKey<TAuthService>(builder, ApiKeyAuthenticationDefaults.AuthenticationScheme, _ => { });
}
public static AuthenticationBuilder AddApiKey<TAuthService>(this AuthenticationBuilder builder, string authenticationScheme)
where TAuthService : class, IApiKeyAuthenticationService
{
return AddApiKey<TAuthService>(builder, authenticationScheme, _ => { });
}
public static AuthenticationBuilder AddApiKey<TAuthService>(this AuthenticationBuilder builder, Action<ApiKeyAuthenticationOptions> configureOptions)
where TAuthService : class, IApiKeyAuthenticationService
{
return AddApiKey<TAuthService>(builder, ApiKeyAuthenticationDefaults.AuthenticationScheme, configureOptions);
}
public static AuthenticationBuilder AddApiKey<TAuthService>(this AuthenticationBuilder builder, string authenticationScheme, Action<ApiKeyAuthenticationOptions> configureOptions)
where TAuthService : class, IApiKeyAuthenticationService
{
builder.Services.AddSingleton<IPostConfigureOptions<ApiKeyAuthenticationOptions>, ApiKeyAuthenticationPostConfigureOptions>();
builder.Services.AddTransient<IApiKeyAuthenticationService, TAuthService>();
return builder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(
authenticationScheme, configureOptions);
}
}
Implement the Authentication service to validate the key, from the request header, against your config file or other store:
public class ApiKeyAuthenticationService : IApiKeyAuthenticationService
{
public Task<bool> IsValidAsync(string apiKey)
{
//Write your validation code here
return Task.FromResult(apiKey == "Test");
}
}
Now, to use it, it is only to add this code at the start:
//register the schema
builder.Services.AddAuthentication(ApiKeyAuthenticationDefaults.AuthenticationScheme)
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, null);
//Register the Authentication service Handler that will be consumed by the handler.
builder.Services.AddSingleton<IApiKeyAuthenticationService,ApiKeyAuthenticationService>();
Or, in a more elegant way, using the extensions:
builder.Services
.AddAuthentication(ApiKeyAuthenticationDefaults.AuthenticationScheme)
.AddApiKey<ApiKeyAuthenticationService>();

Custom IServiceProviderFactory in ASP.NET Core

I wrote a custom IServiceProviderFactory and installed it in Program.cs of a new app like this:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new PropertyInjectingContainerFactory())
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
It does lead to the correct configure function in Startup.cs getting called:
public void ConfigureContainer(PropertyInjectingContainerFactory.Builder builder)
{
builder.AddInjectAttribute<InjectDependencyAttribute>();
}
However, my created container only ever resolves two services: IConfiguration and IHost.
Everything else is resolved by the default container apparantly (for instance a service like ILogger<T> on a controller). What do I do wrong?
Here's the code for my custom factory - and please understand that I probably should be using an existing third-party container, but I also want to understand how this all fits together.
public class PropertyInjectingContainerFactory : IServiceProviderFactory<PropertyInjectingContainerFactory.Builder>
{
public Builder CreateBuilder(IServiceCollection services)
{
return new Builder(services);
}
public IServiceProvider CreateServiceProvider(Builder containerBuilder)
{
return containerBuilder.CreateServiceProvider();
}
public class Builder
{
internal readonly IServiceCollection services;
internal List<Type> attributeTypes = new List<Type>();
public Builder(IServiceCollection services)
{
this.services = services;
}
public Builder AddInjectAttribute<A>()
where A : Attribute
{
attributeTypes.Add(typeof(A));
return this;
}
public IServiceProvider CreateServiceProvider()
=> new PropertyInjectingServiceProvider(services.BuildServiceProvider(), attributeTypes.ToArray());
}
class PropertyInjectingServiceProvider : IServiceProvider
{
private readonly IServiceProvider services;
private readonly Type[] injectAttributes;
public PropertyInjectingServiceProvider(IServiceProvider services, Type[] injectAttributes)
{
this.services = services;
this.injectAttributes = injectAttributes;
}
// This function is only called for `IConfiguration` and `IHost` - why?
public object GetService(Type serviceType)
{
var service = services.GetService(serviceType);
InjectProperties(service);
return service;
}
private void InjectProperties(Object target)
{
var type = target.GetType();
var candidateProperties = type.GetProperties(System.Reflection.BindingFlags.Public);
var props = from p in candidateProperties
where injectAttributes.Any(a => p.GetCustomAttributes(a, true).Any())
select p;
foreach (var prop in props)
{
prop.SetValue(target, services.GetService(prop.PropertyType));
}
}
}
}

GraphQL authentication with Asp.net core using JWT

I am using for GraphQL for .NET package for graphql. But I couldn't understand how can I authentication with JWT in graphql query or mutation.
I read the guide about authorization but I couldn't accomplish.
I need help with GraphQL for .NET authentication.
Any help will be appreciated.
Thanks
The guide is around authorization. The step you're looking for is the authentication and since graphql can be implemented using a ASP.Net API controller, you can implement JWT authentication as you would with any controller.
Here is a sample grapql controller using an Authorize attribute. You could, however, implement this using filter or if you want full control, custom middleware.
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class GraphQLController : ControllerBase
{
private readonly IDocumentExecuter executer;
private readonly ISchema schema;
public GraphQLController(IDocumentExecuter executer, ISchema schema)
{
this.executer = executer;
this.schema = schema;
}
[HttpPost]
public async Task<ActionResult<object>> PostAsync([FromBody]GraphQLQuery query)
{
var inputs = query.Variables.ToInputs();
var queryToExecute = query.Query;
var result = await executer.ExecuteAsync(o => {
o.Schema = schema;
o.Query = queryToExecute;
o.OperationName = query.OperationName;
o.Inputs = inputs;
o.ComplexityConfiguration = new GraphQL.Validation.Complexity.ComplexityConfiguration { MaxDepth = 15};
o.FieldMiddleware.Use<InstrumentFieldsMiddleware>();
}).ConfigureAwait(false);
return this.Ok(result);
}
}
public class GraphQLQuery
{
public string OperationName { get; set; }
public string Query { get; set; }
public Newtonsoft.Json.Linq.JObject Variables { get; set; }
}
In the Startup.cs I have configured JWT bearer token authentication.
Hope this helps.
I myself struggled for two days as well. I'm using https://github.com/graphql-dotnet/authorization now with the setup from this comment (from me): https://github.com/graphql-dotnet/authorization/issues/63#issuecomment-553877731
In a nutshell, you have to set the UserContext for the AuthorizationValidationRule correctly, like so:
public class Startup
{
public virtual void ConfigureServices(IServiceCollection services)
{
...
services.AddGraphQLAuth(_ =>
{
_.AddPolicy("AdminPolicy", p => p.RequireClaim("Role", "Admin"));
});
services.AddScoped<IDependencyResolver>(x => new FuncDependencyResolver(x.GetRequiredService));
services.AddScoped<MySchema>();
services
.AddGraphQL(options => { options.ExposeExceptions = true; })
.AddGraphTypes(ServiceLifetime.Scoped);
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider)
{
...
app.UseMiddleware<MapRolesForGraphQLMiddleware>(); // optional, only when you don't have a "Role" claim in your token
app.UseGraphQL<MySchema>();
...
}
}
public static class GraphQLAuthExtensions
{
public static void AddGraphQLAuth(this IServiceCollection services, Action<AuthorizationSettings> configure)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IAuthorizationEvaluator, AuthorizationEvaluator>();
services.AddTransient<IValidationRule, AuthorizationValidationRule>();
services.AddTransient<IUserContextBuilder>(s => new UserContextBuilder<GraphQLUserContext>(context =>
{
var userContext = new GraphQLUserContext
{
User = context.User
};
return Task.FromResult(userContext);
}));
services.AddSingleton(s =>
{
var authSettings = new AuthorizationSettings();
configure(authSettings);
return authSettings;
});
}
}
public class GraphQLUserContext : IProvideClaimsPrincipal
{
public ClaimsPrincipal User { get; set; }
}
public class MapRolesForGraphQLMiddleware
{
private readonly RequestDelegate _next;
public MapRolesForGraphQLMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
// custom mapping code to end up with a "Role" claim
var metadata = context.User.Claims.SingleOrDefault(x => x.Type.Equals("metadata"));
if (metadata != null)
{
var roleContainer = JsonConvert.DeserializeObject<RoleContainer>(metadata.Value);
(context.User.Identity as ClaimsIdentity).AddClaim(new Claim("Role", string.Join(", ", roleContainer.Roles)));
}
await _next(context);
}
}
public class RoleContainer
{
public String[] Roles { get; set; }
}

Add user to local database after registration in Stormpath

I want to add new user to my local database after register in Stormpath. In doc https://docs.stormpath.com/dotnet/aspnetcore/latest/registration.html#registration is section about post-registration handler. I have problem becouse i can't use UserRepository in StartUp file.
I have error:
Unable to resolve service for type
'AppProject.Repositories.IUserRepository' while attempting to
activate 'AppProject.Startup'
.
public void ConfigureServices(IServiceCollection services, IUserRepository userRepository)
{
services.AddStormpath(new StormpathOptions()
{
Configuration = new StormpathConfiguration()
{
Client = new ClientConfiguration()
{
ApiKey = new ClientApiKeyConfiguration()
{
Id = "xxxxxxxxxxx",
Secret = "xxxxxxxxx"
}
}
},
PostRegistrationHandler = (context, ct) =>
{
return MyPostRegistrationHandler(context, ct, userRepository);
}
});
}
private Task MyPostRegistrationHandler(PostRegistrationContext context, CancellationToken ct, IUserRepository userRepository)
{
userRepository.Add(new User(context.Account.Email, context.Account.FullName, context.Account.GivenName, context.Account.Surname, context.Account.Username));
userRepository.SaveChangesAsync();
return Task.FromResult(0);
}
In this scenario, I don't think it can resolve dependency of IUserRepository in StartUp. You can try something like this.
1) Add an extension method.
public static IServiceProvider AddServices(this IServiceCollection services)
{
services.AddTransient<IUserRepository, UserRepository>();
// rest of the things.
return services.BuildServiceProvider();
}
2) Get the userRepository instance like like this.
IServiceCollection services = new ServiceCollection();
services.AddServices();
var provider = services.BuildServiceProvider();
var userRepository = provider.GetRequiredService<IUserRepository>();
ConfigurationServices will not have IUserRepository input parameter.