Securing Services
Securing your business services to make sure that users can perform only their authorized functions and can access only the allowed data is essential for any business application.
Since Xomega Framework promotes a services architecture, where the same business services can be reused with various multi-tier architectures, you would want to secure each operation as part of its implementation rather than on the remote API level. This would allow you to reuse your service security logic with whichever architecture you decide to use.
Services security
Xomega Framework supports standard .NET claims-based security for securing business services. The base service in the Xomega Framework provides access to the current principal for any inheriting services through its property CurrentPrincipal
. The latter is retrieved from the service provider as follows.
- If the DI container has an instance of the
IPrincipal
service, then it will use that instance. - If the DI container has an
IPrincipalProvider
service, then it will use theCurrentPrincipal
of that service. - If none of the above is available, the
Thread.CurrentPrincipal
will be used.
Using Thread.CurrentPrincipal
to get the current principal may not work well in async methods and services.
Securing operation access
To verify that the current user is authorized to perform any of the business functions that the service operation performs, including calling the operation in general, you should use the claims from the CurrentPrincipal
to perform the security check, and report a critical error of type ErrorType.Security
, if security check fails.
For example, if an operation is only allowed to be called by users that have an Employee
role, then you should add that security check at the top of the implementation method, as follows.
public virtual async Task<Output<ICollection<SalesOrder_ReadListOutput>>> ReadListAsync(
SalesOrder_ReadListInput_Criteria _criteria, CancellationToken token = default)
{
try
{
if (!CurrentPrincipal.IsInRole(Roles.Employee))
{
currentErrors.CriticalError(ErrorType.Security, Messages.OperationNotAllowed);
}
...
}
Using ErrorType.Security
will result in the HTTP status code 403 (Forbidden) when the service is called via REST API.
You can use as generic or as specific an error message as your security requirements allow. In the example above we used an error code as a resource key for a generic message, following the message localization best practices.
Alternatively, you can throw a custom exception instead, e.g. SecurityCheckException
, and implement a custom error parser, which would add a security message to the error list for this exception. This will allow you to output a more detailed message in a test environment, and a generic message in the production environment.
Securing data access
In addition to making sure that the user can perform only authorized functions, you need to make sure that the user can also see only the data that they are allowed to see. This is especially important when your application allows access to external users that should be able to see only their data, but not that of other users.
To restrict access to your data to authorized users only, you would typically get the ID associated with the user from the CurrentPrincipal
's claims, such as the user ID or a customer ID, and use it to restrict the results to only the data that the user is allowed to access.
In the following example, when reading a list of sales orders, we get the personId
from the current user's claims and add it as a filter to return only the sales orders associated with the current user.
public virtual async Task<Output<ICollection<SalesOrder_ReadListOutput>>> ReadListAsync(
SalesOrder_ReadListInput_Criteria _criteria, CancellationToken token = default)
{
var src = from obj in ctx.SalesOrder select obj;
// get the person ID associated with the current user from the claims
var identity = CurrentPrincipal.Identity as ClaimsIdentity;
var idClaim = identity?.Claims?.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
if (int.TryParse(idClaim?.Value, out int personId))
{
// filter sales orders for the customer associated with the current user
src = src.Where(o => o.CustomerObject.PersonObject.BusinessEntityId == personId);
}
...
}
To simplify writing your security checks, you may want to consider adding extension methods to the IPrincipal
interface, which would access the values of the needed claims, check proper roles, etc. This way, for instance, in the code above you'd be able to get the person ID, as follows.
int? personId = CurrentPrincipal.GetPersonId();
Principal providers
As you saw above, for the services to access the current principal, your startup class needs to register with the DI container either a method to get an instance of the current IPrincipal
, or an implementation of the IPrincipalProvider
interface that is defined in the Xomega.Framework
namespace.
Xomega Framework provides a default implementation class called DefaultPrincipalProvider
, which exposes the CurrentPrincipal
property. You can register this provider with the DI container for the current context, and use it to set the current principal in your code based on the specific technology you are using.
Principal in ASP.NET Core
For ASP.NET Core applications, such as Blazor Server or when hosting your business services as a WebAPI, you can register a ContextPrincipalProvider
provided by Xomega Framework as a transient service, which pulls the current principal from the current HttpContext
, as shown below.
services.AddTransient<IPrincipalProvider, ContextPrincipalProvider>();
Persisting auth state provider
If you have a Blazor Server application hosting a Blazor WebAssembly application, then you may need to also register an AuthenticationStateProvider
that persists authentication state on the server, so that it could be used on the WebAssembly.
If you store authentication state in a UserInfo
structure, then you need to register PersistingAuthStateProvider<UserInfo>
as the AuthenticationStateProvider
, as well as an implementation of IPrincipalConverter<UserInfo>
as follows.
services.AddScoped<AuthenticationStateProvider, PersistingAuthStateProvider<UserInfo>>();
services.AddSingleton<IPrincipalConverter<UserInfo>, UserInfoPrincipalConverter>();
Revalidating auth state provider
If you just have a Blazor Server application, then you may also need to revalidate the authentication state periodically, since it would be stored in long-lived circuits and you may need to make sure that the user authentication state is still valid.
In this case, you can create a subclass of the PersistingAuthStateProvider<UserInfo>
and override its method ValidateAuthenticationStateAsync
, which returns true
by default. You can also override the RevalidationInterval
property (which defaults to 30 minutes) to revalidate authentication state at a different frequency.
Principal in WCF apps
WCF services don't support dependency injection by default. If you want to expose your business services via WCF, you'll need to use Xomega Framework support for WCF, and register a scoped DefaultPrincipalProvider
, as shown below, which Xomega Framework will use to set the current principal from the current ServiceSecurityContext
.
services.AddScoped<IPrincipalProvider, DefaultPrincipalProvider>();
Principal in Blazor WebAssembly
Normally, you would not have your business services running in WebAssembly (Wasm). Instead, your business services would be hosted as an ASP.NET Core app, which the Blazor Wasm app would access via a REST API from the browser.
However, to provide the current principal to any Xomega Framework objects and services on the client side of the Blazor Wasm app, the framework offers a class PrincipalAuthStateProvider
, which doubles as both AuthenticationStateProvider
and IPrincipalProvider
.
You can register it with your DI container in the main Program
class of your Wasm app as a concrete implementation for both of those classes, as follows.
services.AddSingleton<AuthenticationStateProvider, PrincipalAuthStateProvider>();
services.AddSingleton(sp => sp.GetService<AuthenticationStateProvider>() as IPrincipalProvider);
Persisted auth state provider
If your Blazor WebAssembly application is hosted by a Blazor Server application, then you need to register PersistedAuthStateProvider<UserInfo>
as an AuthenticationStateProvider
instead, as well as an implementation of IPrincipalConverter<UserInfo>
as follows.
services.AddSingleton<AuthenticationStateProvider, PersistedAuthStateProvider<UserInfo>>();
services.AddSingleton<IPrincipalConverter<UserInfo>, UserInfoPrincipalConverter>();
services.AddSingleton(sp => sp.GetService<AuthenticationStateProvider>() as IPrincipalProvider);
This provider extends PrincipalAuthStateProvider
and uses authentication state persisted by the server, so that it could be transferred seamlessly to the WebAssembly.
Principal in WPF apps
Just like with the Blazor Wasm, multi-tier WPF apps would not run any business services, which would be running separately, and accessed by the WPF client via REST API or WCF. The only instance when WPF apps would have embedded business services is with two-tier client-server applications.
By default, WPF apps don't have dependency injection enabled, so you'll need to set it up manually in your app. Since WPF apps are single-user applications, you would register the DefaultPrincipalProvider
as a singleton with your DI container, as follows.
services.AddSingleton<IPrincipalProvider, DefaultPrincipalProvider>();
Once your app authenticates the current user, it will need to get the current IPrincipalProvider
service from the service provider and set the CurrentPrincipal
to the authenticated user.