Override AuthorizeAttribute in ASP.Net Core and respond Json status - asp.net-core

I'm moving from ASP.Net Framework to ASP.Net Core.
In ASP.Net Framework with Web API 2 project, I can customize AuthorizeAttribute like this :
public class ApiAuthorizeAttribute : AuthorizationFilterAttribute
{
#region Methods
/// <summary>
/// Override authorization event to do custom authorization.
/// </summary>
/// <param name="httpActionContext"></param>
public override void OnAuthorization(HttpActionContext httpActionContext)
{
// Retrieve email and password.
var accountEmail =
httpActionContext.Request.Headers.Where(
x =>
!string.IsNullOrEmpty(x.Key) &&
x.Key.Equals("Email"))
.Select(x => x.Value.FirstOrDefault())
.FirstOrDefault();
// Retrieve account password.
var accountPassword =
httpActionContext.Request.Headers.Where(
x =>
!string.IsNullOrEmpty(x.Key) &&
x.Key.Equals("Password"))
.Select(x => x.Value.FirstOrDefault()).FirstOrDefault();
// Account view model construction.
var filterAccountViewModel = new FilterAccountViewModel();
filterAccountViewModel.Email = accountEmail;
filterAccountViewModel.Password = accountPassword;
filterAccountViewModel.EmailComparision = TextComparision.Equal;
filterAccountViewModel.PasswordComparision = TextComparision.Equal;
// Find the account.
var account = RepositoryAccount.FindAccount(filterAccountViewModel);
// Account is not found.
if (account == null)
{
// Treat the account as unthorized.
httpActionContext.Response = httpActionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
return;
}
// Role is not defined which means the request is allowed.
if (_roles == null)
return;
// Role is not allowed
if (!_roles.Any(x => x == account.Role))
{
// Treat the account as unthorized.
httpActionContext.Response = httpActionContext.Request.CreateResponse(HttpStatusCode.Forbidden);
return;
}
// Store the requester information in action argument.
httpActionContext.ActionArguments["Account"] = account;
}
#endregion
#region Properties
/// <summary>
/// Repository which provides function to access account database.
/// </summary>
public IRepositoryAccount RepositoryAccount { get; set; }
/// <summary>
/// Which role can be allowed to access server.
/// </summary>
private readonly byte[] _roles;
#endregion
#region Constructor
/// <summary>
/// Initialize instance with default settings.
/// </summary>
public ApiAuthorizeAttribute()
{
}
/// <summary>
/// Initialize instance with allowed role.
/// </summary>
/// <param name="roles"></param>
public ApiAuthorizeAttribute(byte[] roles)
{
_roles = roles;
}
#endregion
}
In my customized AuthorizeAttribute, I can check whether account is valid or not and return HttpStatusCode with message to client.
I'm trying to do the samething in ASP.Net Core, but no OnAuthorization for me to override.
How can I achieve the same thing as in ASP.Net Framework ?
Thank you,

You're approaching this incorrectly. It never was really encouraged to write custom attributes for this, or to extend existing. With ASP.NET Core roles are still apart of the system for backwards compatibility but they are now also discouraged.
There is a great 2 part series on some of the driving architecture changes and the way that this is and should be utilized found here. If you want to still rely on roles you can do so, but I would suggest using policies.
To wire a policy do the following:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy(nameof(Policy.Account),
policy => policy.Requirements.Add(new AccountRequirement()));
});
services.AddSingleton<IAuthorizationHandler, AccountHandler>();
}
I created a Policy enum for convenience.
public enum Policy { Account };
Decorate entry points as such:
[
HttpPost,
Authorize(Policy = nameof(Policy.Account))
]
public async Task<IActionResult> PostSomething([FromRoute] blah)
{
}
The AccountRequirement is just a placeholder, it needs to implement the IAuthorizationRequirement interface.
public class AccountRequirement: IAuthorizationRequirement { }
Now we simply need to create a handler and wire this up for DI.
public class AccountHandler : AuthorizationHandler<AccountRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
AccountRequirement requirement)
{
// Your logic here... or anything else you need to do.
if (context.User.IsInRole("fooBar"))
{
// Call 'Succeed' to mark current requirement as passed
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Additional Resources
ASP.NET Core Security -- All the things

My comment looks bad as a comment so I post an answer but only useful if you use MVC:
// don't forget this
services.AddSingleton<IAuthorizationHandler, MyCustomAuthorizationHandler>();
services
.AddMvc(config => { var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser() .AddRequirements(new[] { new MyCustomRequirement() })
.Build(); config.Filters.Add(new AuthorizeFilter(policy)); })
I also noticed that async keyword is superfluous for "HandleRequirementAsync" signature, in question code. And I guess that returning Task.CompletedTask could be good.

Related

.Net Core - Policy based authorization returning 500 on failed authorization

I've set up a simple policy based authorization for testing purposes. The authorization works, as it breaks inside the handler. If I set the test authorization to pass, I'm allowed to access the controller endpoint I set to use the TestPolicy. However, if I set it to fail like in the code snippet below, the authorization fails, but I get a 500-response where clearly a 401 or 403 would be appropriate.
I'm fairly sure it's because the API doesn't have any form of authentication. But I do not want to implement any authentication in this API, as all calls to it should be treated as if they are authenticated. Roles etc. will be fetched through the httpcontext. Does anyone know how I can address this problem without requiring the API consumer to authenticate themselves?
The test code is below. The parameter to new TestRequirement() can be changed to test passing/failing authorization easily.
public static void Configure(IServiceCollection services, IConfiguration configuration)
{
services.AddAuthorization(options =>
{
options.AddPolicy("TestPolicy",
policy => policy.Requirements.Add(new TestRequirement(false)));
});
services.AddSingleton<IAuthorizationHandler, TestHandler>();
//<snip>
}
public class TestHandler : AuthorizationHandler<TestRequirement>
{
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TestRequirement requirement)
{
if (requirement.Passes)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
public class TestRequirement : IAuthorizationRequirement
{
public bool Passes { get; }
public TestRequirement(bool shouldPass)
{
this.Passes = shouldPass;
}
}
Edit: I ended up making a custom authenticationhandler, like this:
public class AuthenticationSchemeHandler : IAuthenticationHandler
{
private HttpContext httpContext;
/// <inheritdoc />
public Task<AuthenticateResult> AuthenticateAsync()
=> Task.FromResult(AuthenticateResult.NoResult());
/// <inheritdoc />
public Task ChallengeAsync(AuthenticationProperties properties)
{
properties ??= new AuthenticationProperties();
this.httpContext.Response.StatusCode = (int) HttpStatusCode.Unauthorized;
return Task.CompletedTask;
}
/// <inheritdoc />
public Task ForbidAsync(AuthenticationProperties properties)
{
properties ??= new AuthenticationProperties();
this.httpContext.Response.StatusCode = (int) HttpStatusCode.Forbidden;
return Task.CompletedTask;
}
/// <inheritdoc />
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
this.httpContext = context;
return Task.CompletedTask;
}
}
It seems to work (although it feels kinda wrong to implement and use authentication when I don't actually authenticate anything), however when my testhandler fails, it'll call the challengeasync instead of forbidasync. From my understanding challengeasync is for when you do not know the user and need to verify them, while forbidasync is when the use is unauthorized. So just scratching my head a bit.

Issues in using Cofoundry and individual user acount Authentication and Authorisation in same Web application

If I add
services.AddControllersWithViews()
.AddCofoundry(Configuration);
to my Startup.cs my Authentication and Authorisation is failing.
If I disable the above lines in Startup.cs #if (SignInManager.IsSignedIn(User)) is true, however if I uncomment it #if (SignInManager.IsSignedIn(User)) is always false despite the user being logged in.
Is it not possible to use Authorisation, Authentication and Cofoundry in the same application?
Cofoundry has it's own user management system and adds authentication automatically to the pipeline to support this. See the User Area docs on how to create your own custom user areas.
There's not much support for running your own authentication mechanisms alongside Cofoundry at this time(improvements are planned), but you can override the existing auth registration and customize it to your needs by implementing your own IAuthConfiguration implementation, including the code in DefaultAuthConfiguration to ensure Cofoundry is configured correctly. For example:
using Cofoundry.Core.DependencyInjection;
using Cofoundry.Domain;
using Cofoundry.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
/// <summary>
/// Use an IDependencyRegistration instance to override the default implementation
/// </summary>
public class AuthDependencyRegistration : IDependencyRegistration
{
public void Register(IContainerRegister container)
{
container.Register<IAuthConfiguration, CustomAuthConfiguration>(RegistrationOptions.Override());
}
}
/// <summary>
/// This is a copy of the default config
/// </summary>
public class CustomAuthConfiguration : IAuthConfiguration
{
private readonly IUserAreaDefinitionRepository _userAreaDefinitionRepository;
private readonly IAuthCookieNamespaceProvider _authCookieNamespaceProvider;
public CustomAuthConfiguration(
IUserAreaDefinitionRepository userAreaDefinitionRepository,
IAuthCookieNamespaceProvider authCookieNamespaceProvider
)
{
_userAreaDefinitionRepository = userAreaDefinitionRepository;
_authCookieNamespaceProvider = authCookieNamespaceProvider;
}
public void Configure(IMvcBuilder mvcBuilder)
{
var services = mvcBuilder.Services;
var allUserAreas = _userAreaDefinitionRepository.GetAll();
// Set default schema as specified in config, falling back to CofoundryAdminUserArea
// Since any additional areas are configured by the implementor there shouldn't be multiple
// unless the developer has misconfigured their areas.
var defaultSchemaCode = allUserAreas
.OrderByDescending(u => u.IsDefaultAuthSchema)
.ThenByDescending(u => u is CofoundryAdminUserArea)
.ThenBy(u => u.Name)
.Select(u => u.UserAreaCode)
.First();
var defaultScheme = CofoundryAuthenticationConstants.FormatAuthenticationScheme(defaultSchemaCode);
var authBuilder = mvcBuilder.Services.AddAuthentication(defaultScheme);
var cookieNamespace = _authCookieNamespaceProvider.GetNamespace();
foreach (var userAreaDefinition in allUserAreas)
{
var scheme = CofoundryAuthenticationConstants.FormatAuthenticationScheme(userAreaDefinition.UserAreaCode);
authBuilder
.AddCookie(scheme, cookieOptions =>
{
cookieOptions.Cookie.Name = cookieNamespace + userAreaDefinition.UserAreaCode;
cookieOptions.Cookie.HttpOnly = true;
cookieOptions.Cookie.IsEssential = true;
cookieOptions.Cookie.SameSite = SameSiteMode.Lax;
if (!string.IsNullOrWhiteSpace(userAreaDefinition.LoginPath))
{
cookieOptions.LoginPath = userAreaDefinition.LoginPath;
}
});
}
mvcBuilder.Services.AddAuthorization();
}
}

Set custom claims for ClaimsIdentity using IdentityServer4 authentication

I have an ASP.NET Core 2.1 application authenticated using IdentityServer4.TokenValidation
authenticationBuilder.AddIdentityServerAuthentication(AuthorizationConstants.IpreoAccountAuthenticationScheme, options =>
{
options.RequireHttpsMetadata = false;
options.ApiName = apiName;
options.ApiSecret = apiSecret;
options.Authority = authority;
options.LegacyAudienceValidation = true;
});
What is the best way how can I add custom claims to identity?
Taking into account that we still need to have an opportunity to use Authorize attribute with Roles validation.
For bearer authentication for example we can use OnTokenValidated handler which is fired on each request. But for IdentityServerAuthenticationOptions Events property is of type of object and initializing it with a dummy object with OnTokenValidated property does not work.
We have to support JWT and reference tokens.
Also we need to support multiple authentication schemes
Any ideas or suggestions?
Ruard van Elburg gave me a good idea about using a middleware. The only thing I had to update use this approach for multiple authentication schemes was overriding IAuthenticationSchemeProvider to keep using UseAuthentication middleware.
https://github.com/aspnet/Security/blob/beaa2b443d46ef8adaf5c2a89eb475e1893037c2/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs
So it returns default scheme based on a request content
What I had to do:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.UseMiddleware<ClaimsMiddleware>(); // to set claims for authenticated user
app.UseMvc();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddTransient<IAuthenticationSchemeProvider, CustomAuthenticationSchemeProvider>();
services.AddAuthorization();
services.AddAuthentication // add authentication for multiple schemes
}
You will need middleware for that. As an example, I suggest you take a look at the PolicyServer. It has the same approach.
IdentityServer handles the authentication, while authorization is handled by the PolicyServer. The free OSS version adds claims in the middleware.
From the source code:
/// Add the policy server claims transformation middleware to the pipeline.
/// This middleware will turn application roles and permissions into claims
/// and add them to the current user
public static IApplicationBuilder UsePolicyServerClaims(this IApplicationBuilder app)
{
return app.UseMiddleware<PolicyServerClaimsMiddleware>();
}
Where PolicyServerClaimsMiddleware is:
public class PolicyServerClaimsMiddleware
{
private readonly RequestDelegate _next;
/// <summary>
/// Initializes a new instance of the <see cref="PolicyServerClaimsMiddleware"/> class.
/// </summary>
/// <param name="next">The next.</param>
public PolicyServerClaimsMiddleware(RequestDelegate next)
{
_next = next;
}
/// <summary>
/// Invoke
/// </summary>
/// <param name="context">The context.</param>
/// <param name="client">The client.</param>
/// <returns></returns>
public async Task Invoke(HttpContext context, IPolicyServerRuntimeClient client)
{
if (context.User.Identity.IsAuthenticated)
{
var policy = await client.EvaluateAsync(context.User);
var roleClaims = policy.Roles.Select(x => new Claim("role", x));
var permissionClaims = policy.Permissions.Select(x => new Claim("permission", x));
var id = new ClaimsIdentity("PolicyServerMiddleware", "name", "role");
id.AddClaims(roleClaims);
id.AddClaims(permissionClaims);
context.User.AddIdentity(id);
}
await _next(context);
}
}
And from startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore(options =>
{
// workaround: https://github.com/aspnet/Mvc/issues/7809
options.AllowCombiningAuthorizeFilters = false;
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddAuthorization();
// This is not relevant for you, but just to show how policyserver is implemented.
// The bottom line is that you can implement this anyway you like.
// this sets up the PolicyServer client library and policy
// provider - configuration is loaded from appsettings.json
services.AddPolicyServerClient(Configuration.GetSection("Policy"))
.AddAuthorizationPermissionPolicies();
}
public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
// add this middleware to make roles and permissions available as claims
// this is mainly useful for using the classic [Authorize(Roles="foo")] and IsInRole functionality
// this is not needed if you use the client library directly or the new policy-based authorization framework in ASP.NET Core
app.UsePolicyServerClaims();
app.UseMvc();
}

openid connect - identifying tenant during login

I have a multi-tenant (single database) application which allows for same username/email across different tenants.
At the time of login (Implicit flow) how can I identify the tenant? I thought of following possibilities:
At the time of registration ask the user for account slug (company/tenant slug) and during login user should provide the slug along with username and password.
But there is no parameter in open id request to send the slug.
Create an OAuth application at the time of registration and use slug as client_id. At the time of login pass slug in client_id, which I will use to fetch the tenant Id and proceed further to validate the user.
Is this approach fine?
Edit:
Also tried making slug part of route param
.EnableTokenEndpoint("/connect/{slug}/token");
but openiddict doesn't support that.
Edit: this answer was updated to use OpenIddict 3.x.
The approach suggested by McGuire will work with OpenIddict (you can access the acr_values property via OpenIddictRequest.AcrValues) but it's not the recommended option (it's not ideal from a security perspective: since the issuer is the same for all the tenants, they end up sharing the same signing keys).
Instead, consider running an issuer per tenant. For that, you have at least 2 options:
Give OrchardCore's OpenID module a try: it's based on OpenIddict and natively supports multi-tenancy. It's still in beta but it's actively developed.
Override the options monitor used by OpenIddict to use per-tenant options.
Here's a simplified example of the second option, using 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.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseStatusCodePagesWithReExecute("/error");
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(options =>
{
options.MapControllers();
options.MapDefaultControllerRoute();
});
}
Implement a custom IOptionsMonitor<OpenIddictServerOptions>:
public class OpenIddictServerOptionsProvider : IOptionsMonitor<OpenIddictServerOptions>
{
private readonly ConcurrentDictionary<(string Name, string Tenant), Lazy<OpenIddictServerOptions>> _cache;
private readonly IOptionsFactory<OpenIddictServerOptions> _optionsFactory;
private readonly TenantProvider _tenantProvider;
public OpenIddictServerOptionsProvider(
IOptionsFactory<OpenIddictServerOptions> optionsFactory,
TenantProvider tenantProvider)
{
_cache = new ConcurrentDictionary<(string, string), Lazy<OpenIddictServerOptions>>();
_optionsFactory = optionsFactory;
_tenantProvider = tenantProvider;
}
public OpenIddictServerOptions CurrentValue => Get(Options.DefaultName);
public OpenIddictServerOptions Get(string name)
{
var tenant = _tenantProvider.GetCurrentTenant();
Lazy<OpenIddictServerOptions> Create() => new(() => _optionsFactory.Create(name));
return _cache.GetOrAdd((name, tenant), _ => Create()).Value;
}
public IDisposable OnChange(Action<OpenIddictServerOptions, string> listener) => null;
}
Implement a custom IConfigureNamedOptions<OpenIddictServerOptions>:
public class OpenIddictServerOptionsInitializer : IConfigureNamedOptions<OpenIddictServerOptions>
{
private readonly TenantProvider _tenantProvider;
public OpenIddictServerOptionsInitializer(TenantProvider tenantProvider)
=> _tenantProvider = tenantProvider;
public void Configure(string name, OpenIddictServerOptions options) => Configure(options);
public void Configure(OpenIddictServerOptions options)
{
var tenant = _tenantProvider.GetCurrentTenant();
// Resolve the signing credentials associated with the tenant (in a real world application,
// the credentials would be retrieved from a persistent storage like a database or a key vault).
options.SigningCredentials.Add(tenant switch
{
"fabrikam" => new(new RsaSecurityKey(RSA.Create(keySizeInBits: 2048)), SecurityAlgorithms.RsaSha256),
_ => new(new RsaSecurityKey(RSA.Create(keySizeInBits: 2048)), SecurityAlgorithms.RsaSha256)
});
// Resolve the encryption credentials associated with the tenant (in a real world application,
// the credentials would be retrieved from a persistent storage like a database or a key vault).
options.EncryptionCredentials.Add(tenant switch
{
"fabrikam" => new(new RsaSecurityKey(RSA.Create(keySizeInBits: 2048)),
SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512),
_ => new(new RsaSecurityKey(RSA.Create(keySizeInBits: 2048)),
SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512)
});
// Other tenant-specific options can be registered here.
}
}
Register the services in your DI container:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
// Enable the authorization, device, introspection,
// logout, token, userinfo and verification endpoints.
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetDeviceEndpointUris("/connect/device")
.SetIntrospectionEndpointUris("/connect/introspect")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo")
.SetVerificationEndpointUris("/connect/verify");
// Note: this sample uses the code, device code, password and refresh token flows, but you
// can enable the other flows if you need to support implicit or client credentials.
options.AllowAuthorizationCodeFlow()
.AllowDeviceCodeFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow();
// Mark the "email", "profile", "roles" and "demo_api" scopes as supported scopes.
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles, "demo_api");
// Force client applications to use Proof Key for Code Exchange (PKCE).
options.RequireProofKeyForCodeExchange();
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
options.UseAspNetCore()
.EnableStatusCodePagesIntegration()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableVerificationEndpointPassthrough();
});
services.AddSingleton<TenantProvider>();
services.AddSingleton<IOptionsMonitor<OpenIddictServerOptions>, OpenIddictServerOptionsProvider>();
services.AddSingleton<IConfigureOptions<OpenIddictServerOptions>, OpenIddictServerOptionsInitializer>();
}
To confirm this works correctly, navigate to https://localhost:[port]/fabrikam/.well-known/openid-configuration (you should get a JSON response with the OpenID Connect metadata).
For anyone who's interested in an alternative approach (more of an extension) to Kevin Chalet's accepted answer look at the pattern described here using a custom implementation of IOptions<TOption> as MultiTenantOptionsManager<TOptions> https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/master/docs/Options.md
The authentication sample for the same pattern is here https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/master/docs/Authentication.md
The full source code for the implemenation is here https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/7bc72692b0f509e0348fe17dd3248d35f4f2b52c/src/Finbuckle.MultiTenant.Core/Options/MultiTenantOptionsManager.cs
The trick is using a custom IOptionsMonitorCache that is tenant aware and always returns a tenant scoped result https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/7bc72692b0f509e0348fe17dd3248d35f4f2b52c/src/Finbuckle.MultiTenant.Core/Options/MultiTenantOptionsCache.cs
internal class MultiTenantOptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new()
{
private readonly IOptionsFactory<TOptions> _factory;
private readonly IOptionsMonitorCache<TOptions> _cache; // Note: this is a private cache
/// <summary>
/// Initializes a new instance with the specified options configurations.
/// </summary>
/// <param name="factory">The factory to use to create options.</param>
public MultiTenantOptionsManager(IOptionsFactory<TOptions> factory, IOptionsMonitorCache<TOptions> cache)
{
_factory = factory;
_cache = cache;
}
public TOptions Value
{
get
{
return Get(Microsoft.Extensions.Options.Options.DefaultName);
}
}
public virtual TOptions Get(string name)
{
name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
// Store the options in our instance cache.
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
public void Reset()
{
_cache.Clear();
}
}
public class MultiTenantOptionsCache<TOptions> : IOptionsMonitorCache<TOptions> where TOptions : class
{
private readonly IMultiTenantContextAccessor multiTenantContextAccessor;
// The object is just a dummy because there is no ConcurrentSet<T> class.
//private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, object>> _adjustedOptionsNames =
// new ConcurrentDictionary<string, ConcurrentDictionary<string, object>>();
private readonly ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>> map = new ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>>();
public MultiTenantOptionsCache(IMultiTenantContextAccessor multiTenantContextAccessor)
{
this.multiTenantContextAccessor = multiTenantContextAccessor ?? throw new ArgumentNullException(nameof(multiTenantContextAccessor));
}
/// <summary>
/// Clears all cached options for the current tenant.
/// </summary>
public void Clear()
{
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
cache.Clear();
}
/// <summary>
/// Clears all cached options for the given tenant.
/// </summary>
/// <param name="tenantId">The Id of the tenant which will have its options cleared.</param>
public void Clear(string tenantId)
{
tenantId = tenantId ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
cache.Clear();
}
/// <summary>
/// Clears all cached options for all tenants and no tenant.
/// </summary>
public void ClearAll()
{
foreach(var cache in map.Values)
cache.Clear();
}
/// <summary>
/// Gets a named options instance for the current tenant, or adds a new instance created with createOptions.
/// </summary>
/// <param name="name">The options name.</param>
/// <param name="createOptions">The factory function for creating the options instance.</param>
/// <returns>The existing or new options instance.</returns>
public TOptions GetOrAdd(string name, Func<TOptions> createOptions)
{
if (createOptions == null)
{
throw new ArgumentNullException(nameof(createOptions));
}
name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
return cache.GetOrAdd(name, createOptions);
}
/// <summary>
/// Tries to adds a new option to the cache for the current tenant.
/// </summary>
/// <param name="name">The options name.</param>
/// <param name="options">The options instance.</param>
/// <returns>True if the options was added to the cache for the current tenant.</returns>
public bool TryAdd(string name, TOptions options)
{
name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
return cache.TryAdd(name, options);
}
/// <summary>
/// Try to remove an options instance for the current tenant.
/// </summary>
/// <param name="name">The options name.</param>
/// <returns>True if the options was removed from the cache for the current tenant.</returns>
public bool TryRemove(string name)
{
name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
return cache.TryRemove(name);
}
}
The advantage is you don't have to extend every type of IOption<TOption>.
It can be hooked up as shown in the example https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/3c94ab2848758de7c9d0154aeddd4820dd545fbf/src/Finbuckle.MultiTenant.Core/DependencyInjection/MultiTenantBuilder.cs#L71
private static MultiTenantOptionsManager<TOptions> BuildOptionsManager<TOptions>(IServiceProvider sp) where TOptions : class, new()
{
var cache = ActivatorUtilities.CreateInstance(sp, typeof(MultiTenantOptionsCache<TOptions>));
return (MultiTenantOptionsManager<TOptions>)
ActivatorUtilities.CreateInstance(sp, typeof(MultiTenantOptionsManager<TOptions>), new[] { cache });
}
Using it https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/3c94ab2848758de7c9d0154aeddd4820dd545fbf/src/Finbuckle.MultiTenant.Core/DependencyInjection/MultiTenantBuilder.cs#L43
public static void WithPerTenantOptions<TOptions>(Action<TOptions, TenantInfo> tenantInfo) where TOptions : class, new()
{
// Other required services likes custom options factory, see the linked example above for full code
Services.TryAddScoped<IOptionsSnapshot<TOptions>>(sp => BuildOptionsManager<TOptions>(sp));
Services.TryAddSingleton<IOptions<TOptions>>(sp => BuildOptionsManager<TOptions>(sp));
}
Every time IOptions<TOption>.Value is called it looks up the multi tenant aware cache to retrieve it. So you can conveniently use it in singletons like the IAuthenticationSchemeProvider as well.
Now you can register your tenant specific OpenIddictServerOptionsProvider options same as the accepted answer.
You're on the right track with the OAuth process. When you register the OpenID Connect scheme in your client web app's startup code, add a handler for the OnRedirectToIdentityProvider event and use that to add your "slug" value as the "tenant" ACR value (something OIDC calls the "Authentication Context Class Reference").
Here's an example of how you'd pass it to the server:
.AddOpenIdConnect("tenant", options =>
{
options.CallbackPath = "/signin-tenant";
// other options omitted
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async context =>
{
string slug = await GetCurrentTenantAsync();
context.ProtocolMessage.AcrValues = $"tenant:{slug}";
}
};
}
You didn't specify what sort of server this is going to, but ACR (and the "tenant" value) are standard parts of OIDC. If you're using Identity Server 4, you could just inject the Interaction Service into the class processing the login and read the Tenant property, which is automatically parsed out of the ACR values for you. This example is non-working code for several reasons, but it demonstrates the important parts:
public class LoginModel : PageModel
{
private readonly IIdentityServerInteractionService interaction;
public LoginModel(IIdentityServerInteractionService interaction)
{
this.interaction = interaction;
}
public async Task<IActionResult> PostEmailPasswordLoginAsync()
{
var context = await interaction.GetAuthorizationContextAsync(returnUrl);
if(context != null)
{
var slug = context.Tenant;
// etc.
}
}
}
In terms of identifying the individual user accounts, your life will be a lot easier if you stick to the OIDC standard of using "subject ID" as the unique user ID. (In other words, make that the key where you store your user data like the tenant "slug", the user email address, password salt and hash, etc.)

Get AuthorizeAttribute to work roles with start and expiration date in web api 2 application ?

I need to modify user roles in my web api 2 project using Identity 2 by adding additional properties: DateTime StartDate and DateTime EndDate. This is required to be able to grant users roles for a limited period of time.
What do I need to do to get the Authorize attribute such as [Authorize(Role="poweruser")] etc. to understand the role dates?
According to source (https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Http/AuthorizeAttribute.cs) this filter ultimately calls IPrincipal.IsInRole:
protected virtual bool IsAuthorized(HttpActionContext actionContext)
{
...
if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole))
{
return false;
}
return true;
}
Looks like I need to subclass the implementation of IPrincipal in HttpActionContext.ControllerContext.RequestContext.Principal and somehow inject it somewhere in the life cycle instead of the default implementation.
How do I do this?
Just Create a custom implementation of of AuthorizeAttribute like UserAuthorize and instead of using [Authorize(Role="poweruser")] you will use [UserAuthorize(Role="poweruser")].
Your UserAuthorize implmentation could look like this:
public class UserAuthorizeAttribute : AuthorizeAttribute
{
/// <summary>
/// Validate User Request for selected Feature
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var isAuthorized = base.AuthorizeCore(httpContext);
if(!isAuthorized) {
return false; //User is Not Even Logged In
}
//Your custom logic here
}