The Security Pipeline
In this article
The ISecurityPipeline is the core ASPSecurityKit engine that drives various checks and operations on the request related to authentication, multi-factor, activity-based & data-aware authorization, IP firewall, account verification, suspension and much more.
Key concepts and components
In this section we’ll introduce you to some common components, concepts and processes which are referred in the stages of the subsequent pipeline sections. But even before that, a few things to note:
- In this guide, we’ll use the common term ‘operation’ to represent both the actions (in ASP.NET Mvc/Api/Core) and the request DTOs (ServiceStack).
- We’ll use the terms platform and framework interchangeably to represent various web frameworks supported by ASPSecurityKit via its framework specific libraries.
- since ASPSecurityKit is designed from the ground up for testability and extensibility all components used in the pipeline execution starting from ISecurityPipeline are injected with their corresponding interfaces and thus it’s easy to replace any component for testing or other purposes. However, to elucidate how a particular process works so you can understand as to how you can extend it or plug into it for your particular requirements, we’ll directly refer to certain implementation classes in this article.
- We’ll only refer to synchronous methods in this guide but all of them do have corresponding asynchronous methods. Refer to the method’s linked docs page for the same.
Enabling the security pipeline protection
To enable security pipeline, you can either decorate your ASP.NET controller or ServiceStack service with the
ProtectAttribute, which is available in all supported platforms (ASP.NET Core, ASP.NET Mvc, ASP.NET Web API and Service Stack), or you can enable it as a global filter.
ISecurityPipeline supports asynchronous execution but whether or not it is executed asynchronously depends on how it’s invoked. For instance, in ASP.NET MVC5, there’s no way to invoke action filters asynchronously so its corresponding ProtectAttribute doesn’t support async. ServiceStack also doesn’t support async operations on filter attributes as of this writing but it does support global async filters, so its corresponding ASPSecurityKitFeature supports registering the pipeline for async execution.
IRequestService is the primary interface that wraps incoming web request and exposes a framework agnostic access to the request properties. Being related to request object which may get initialized during execution rather than service construction, IRequestService is passed as a method parameter to the pipeline components. However, ISecurityContext.RequestService is initialized by the ISecurityPipeline.Execute as a first thing. This way you can inject ISecurityContext and get access to IRequestService in methods during or after pipeline execution.
Another important concept about pipeline to be aware of is RequestFeature. These features represent various options that the operations can enable as needed usually using the corresponding attributes available in the framework libraries. For example, RequestFeature.Anonymous is represented by AllowAnonymousAttribute in both .NET and ServiceStack.
Following the Zero Trust security model,, everything that either reduces the security check or enables additional authentication option has been offloaded as a request feature, and thus requires developers to enable it explicitly for only the intended operations.
The ISecurityContext represents a temporary shared space to set data related to the security for the context of the request. The data is shared by different steps as and when those are executed. For example, the AuthMethod is set by the first scheme handler that recognizes a known auth token in the request, to the value representing the recognized token. While the AuthDetails is set by the AuthenticationProvider upon successful authentication.
If at any point a required security check fails or AuthFailedException error is thrown during the pipeline execution, error handling logic takes over the job of processing and reporting the error properly to the caller.
It’s also useful to point out that the security pipeline doesn’t handle exceptions other than AuthFailedException occurring during the execution. Such unforeseen failures usually mean a bug in the code somewhere and are best to be logged and reported as 500 (internal server error) to the caller by the application. ASPSecurityKit’s Starter and higher source packages come with the logic and types necessary to gracefully handle and report such unhandled exceptions occurring not just in security pipeline but anywhere in the application.
AN important component is ISecurityFailureResponseHandler whose HandleFailure method is first invoked with the request and error information. If it returns true, the error is considered handled and pipeline exits. In case it returns false, one of the following actions is taken:
- If ISecuritySetting.ThrowSecurityFailureAsException is true, an AuthFailedException is thrown with failure details.
- Otherwise, the error is written to the response using IRequestService.WriteToResponse. ISecurityFailureResponseHandler is designed so you can customize error reporting and perform logging related to any failure occurring in the security pipeline. Feel free to replace the default implementation which does nothing (at least in the case of APIs).
For regular (non-json) HTTP requests targeting an MVC action a default implementation – MvcSecurityFailureResponseHandler – is provided that handles redirection to appropriate URL configured in INetSecuritySettings based on the reason of failure. If you’re using MVC in your project, you can choose to derive from this implementation instead to keep the default redirection logic.
ISecurityEvents provides number of methods such as OnAuthenticated, OnAuthorized etc., most of which are invoked by the pipeline after successful completion of the corresponding step. An event will not be invoked if the corresponding step has been ignored. For example, if the requested operation is marked with RequestFeature.MFANotRequired or if MFA isn’t enabled, the OnMultiFactorVerified event won’t be invoked.
These events are designed so you can hook them into the pipeline for custom processing without having to create a derived type of a particular provider. It’s perfectly fine to validate the request in these events and if you see any issue as per your security needs, you can raise AuthFailedException to terminate the pipeline execution with failure details for error handling.
A successful Cross-site scripting (XSS) attack can cause the hacker to steal credentials and other valuable data of targeted users. This section focuses on input validation support provided by ASPSecurityKit to detect potential XSS injection and deny the request. Additional measures are needed in your web application to holistically sanitize data and protect users against XSS. Read the XSS guide to learn more.
ASPSecurityKit provides a XSS validator that can traverse and detect potential XSS injection in the request input. The XSS validation is performed as the first step of the security pipeline, unless you disable it in settings. Only in ASP.NET Framework Mvc, it’s disabled by default because ASP.NET MVC5 has a built-in XSS detection feature which is enabled by default.
If enabled, the XSS validation traverses the string values in the request input (action arguments in ASP.NET and DTO in ServiceStack) recursively and if potential XSS injection is detected, an XssDetectedException is thrown and the same is caught by the
SecurityPipeline. It’s then converted to AuthFailedException with Reason as OpResult.XssDetected, message containing a snippet of value detected with potential XSS and Errors containing more details such as property name, value etc. that failed XSS check. Finally, this exception is handled as described in above error handling section.
The Validate method recursively traverses objects, dictionary values, IEnumerable collections and arrays as long as the item is not a
ValueType and wherever it finds a
string property or collection item, it validates the same for XSS. You can decorate a property with AllowHtmlAttribute if you want the validator to ignore it.
Any operation marked with RequestFeature.Anonymous allows anonymous execution. However, the pipeline still attempts to authenticate the request to initialize the authenticated session. This is done so that frameworks like MVC can render menu items properly even on anonymous pages.
Upon success, the ISecurityEvents.OnAuthenticated method is invoked. But no further steps of the pipeline are executed including multi-factor.
Failure or error occurring while authenticating a request marked with anonymous access are ignored as if the authentication was never attempted, nor the error handling routines are invoked.
Authentication answers the question: ‘Who are you?’ latest web systems are expected to support connectivity with many kinds of callers including browsers, mobile apps, IoT devices, backend jobs, etc. each of which likely needs a different authentication method or scheme to identify itself securely. ASPSecurityKit supports multitude of authentication schemes, tokens and methods embedded in HTTP headers, URL query string and HTTP cookies. Adding support for any custom scheme is also straightforward.
Authenticating the request
AuthenticationProvider handles authentication step of the security pipeline. It takes a collection of IAuthenticationSchemeHandlers and executes them sequentially. As soon as a scheme handler returns AuthSchemeValidationResult.Evaluated as true, the provider stops further evaluation of the remaining handlers. Evaluation doesn’t mean that the authentication succeeded; it just indicates that a relevant token was found in the request.
If the token was invalid AuthSchemeValidationResult.Error holds that information which is returned to the security pipeline for error processing/reporting.
IN case of a valid token, AuthSchemeValidationResult.Auth holds detailed information about this identity token such as associated user, IP firewall rules, validity, sliding, multi-factor state etc. AuthenticationProvider then goes on to initialize the authenticated session (on ISessionService) for the request. For doing so, it first calls IAuthSessionProvider.LoadSession with auth details received and then IUserService.Load. The latter is only called when former has initialized an empty session (IUserService.IsAuthenticated is still
false). If you’re loading an existing session from cache, associated user and permits are already part of the cache (IUserService.IsAuthenticated returns
true), so IUserService.Load won’t be invoked.
IUserService.Load performs a user suspension check which you can disable for all operations using ISecuritySettings.LetSuspendedAuthenticate or for specific operations by marking it with RequestFeature.AllowSuspendedUser. In case both are
false and if user is found suspended, authentication fails with OpResult.Suspended.
ASPSecurityKit supports multiple kinds of authentication tokens. Each such token handler implements the IAuthenticationSchemeHandler interface having only one simple method Validate. We’ve already talked about different kinds of results that is returned by the handler in previous section. If the handler doesn’t detect a matching token in the request, it just returns AuthSchemeValidationResult.NotEvaluated.
Upon detection of matching token in the request, the handler makes a call to IAuthSessionProvider.GetValidAuthDetails passing an authUrn representing the token, to obtain associated identity details. It then validates the token (such as verifying the signature as in case of HMAC tokens) and responds appropriately.
Validating the identity token
IAuthenticationSchemeHandler is only responsible to validate the auth token embedded in the request such as whether or not the signature is valid. Whether the identity represented by this auth token is itself valid for the current request is validated by IAuthSessionProvider.GetValidAuthDetails. This lets you:
- reuse the same validation rules across identity tokens such as regarding validity (effective/expiration), IP firewall, permitted origins (as in case of public keys) etc.
- Extend or modify the validation rules without having to touch the token handlers.
Handling authentication failures
If the authentication fails with a known failure, IAuthenticationProvider.HandleUnauthenticatedRequest is called by the pipeline to process failure. The default implementation handles the failure as described in above error handling section.
Successful authentication event
Upon success, the ISecurityEvents.OnAuthenticated method is invoked. This is where you can do post-authentication processing such as initializing additional security data based on the established identity and user details. You can even throw an AuthFailedException if you consider authentication as invalid.
Multi-Factor Authentication (MFA)
Multi-Factor Authentication (MFA), also popularly known as Two-Factor Authentication (2FA), involves requiring one or more additional evidence from the caller to complete the authentication. This additional evidence is usually dynamic in nature compare to credentials that are static (known in advance to both user and server). This evidence is expected to be delivered/collected from the user’s secondary devices or different communication channels and thus make it difficult to compromise the user’s account.
After establishing the identity with token authentication, the security pipeline verifies the multi-factor status of the request by executing IMultiFactorProvider.Verify. But before doing so, it runs a series of checks to determine whether or not MFA applies to the current request, authenticated identity token, authenticated user etc.
- Determines whether multi-factor is enabled for the application by calling ISecuritySettings.IsMFAEnabled.
- If MFA is enabled, determines whether the requested operation is marked with RequestFeature.MFANotRequired.
- If not, invokes IMultiFactorProvider.IsEnabled to execute additional logic to determine whether MFA is enabled.
Checks performed by MultiFactorProvider.IsEnabled:
- Does the authenticated identity token support MFA? Usually, tokens of type user session need MFA while APIKeys do not.
- Is current authenticated session impersonating some other user? MFA check is skipped during impersonation; because the session doesn’t hold data for the real user in normal form and performing any kind of MFA check on the impersonated user doesn’t make sense. The impersonation information for non-bound impersonated sessions only lives in the session cache which usually has a lifetime similar to that of MFA verification lifetime (about 30 minutes) so not performing repeated MFA check during impersonation shouldn’t be a problem. Regarding bound impersonated sessions, the general recommendation is to create them with an ephemeral lifetime (of about 30 minutes with sliding expiration) only.
- Is operation marked as public? Public operations Do not need MFA check.
- Is MFA enabled for the current user? Until user sets up the MFA settings, there’s no point enforcing it.
- Final check is made by calling IAuthSessionProvider.IsMFAEnabled. This is where you should override and add additional checks mandated by the business. The default implementation of AuthSessionProvider.IsMFAEnabled performs only one check - it considers MFA as disabled if whitelisted networks setting is not null and caller IP is part of any of such networks. This might sound strange but it was a real business requirement in ISCP because of ‘no mobile-phone allowed’ policy at the office.
MultiFactorProvider.Verify makes a call to IUserService.IsMFAVerified to check whether or not current session is already verified with MFA. Upon receiving a valid MFA token, the applications can decide either to tie this verification for the lifetime of the session or have its lifetime. When you make call to IUserService.SetMFAVerified the parameter – validUntilSessionExpired – lets you control this behavior. If you pass it as true, IUserService.IsMFAVerified will always return true for the lifetime of this session. IN case you do not set it or set it as false, the MFA verification will be valid only for a duration of user inactivity as specified by ISecuritySettings.MFAAliveForMinutes. To achieve this, UserService.IsMFAVerified updates the access timestamp (only at most once a minute – to save unnecessary database calls with requests coming in quick succession) by calling IIdentityRepository.SlideRecentAccessWithMFAVerification.
Handling MFA failures
If the MFA verification fails, IMultiFactorProvider.HandleUnverifiedRequest is called by the pipeline to process failure. The default implementation handles the failure as described in above error handling section.
Successful multi-factor verification event
Upon success, the ISecurityEvents.OnMultiFactorVerified method is invoked. This is where you can do post multi-factor verification processing. You can even throw an AuthFailedException if you consider multi-factor verification as invalid.
Authorization answers the question: ‘Are you permitted?’ In a multi-tenanted and multi-user system, not all users are supposed to possess the same privileges. Often there are different levels or categories of users which indicate the number of privileges and type of functions permitted on the system.
Authorizing the request
After establishing the identity with token authentication and verifying the multi-factor (if enabled), the security pipeline moves onto authorizing the authenticated identity for the requested operation. AuthorizationProvider handles authorization step of the security pipeline. It performs a series of following preliminary checks to validate the request before performing the activity-based, data-aware authorization.
- If the current identity token is a public API key, exits with OpResult.Unauthorized failure. This is because public API keys can only be used to access operations marked as RequestFeature.Public.
- If current user’s password has expired and the requested operation is not marked with RequestFeature.AllowPasswordExpired, exits with OpResult.PasswordExpired failure. User must be prompted to set a new password by the UI.
- If user is impersonating and neither the requested operation is marked with RequestFeature.AllowImpersonation nor the request matches any of the allowed operation rules, exits with OpResult.Unauthorized failure.
- If user hasn’t verified with Multi-Factor Authentication (MFA) and MFA is enforced as a policy, exits with OpResult.MustEnableMFA failure. Provided that the requested operation isn’t marked with RequestFeature.MFASetting as such operations are used to enable MFA, and MFA is supported on the current identity token.
- If user verification is mandatory and current user is not verified, exits with OpResult.NotVerified failure. Provided that the requested operation isn’t marked with RequestFeature.VerificationNotRequired – you need to mark operation that allow user to change email, for example, without having to verify first if email verification is what you are enforcing. User could have provided invalid email by mistake.
- Finally, if you want to add more preliminary checks you can do so by overriding OtherChecksBeforeActivityAuthorization and return any existing or custom OpResult value, other than OpResult.Success to indicate failure.
activity-based, data-aware authorization (ADA)
familiarize yourself with ASPSecurityKit’s first of its kind activity-based, data-aware authorization (ADA) feature that gives you granular control on determining not just whether a user is authorized to perform the requested operation, but also whether he’s authorized to perform the requested operation on the data specified.
ADA is skipped if the requested operation is marked with RequestFeature.ActivityAuthorizationNotRequired. Otherwise, a platform-specific implementation of IActivityPermittedHandler.IsPermitted is invoked, which works as follows (simplified; see the how-to ADA article for details):
- Determines a permissionCode for the requested operation based on the activity definition logic.
- If the requested operation has an auth definition, invoke it to determine if current identity is permitted. An auth definition is your custom logic that you write to override the default ADA flow.
- Otherwise, if the requested operation is marked with RequestFeature.PossessesPermissionCheckOnly, determine permit based on whether or not the current identity possesses a permit having the permissionCode obtained above (no data authorization).
- Otherwise, invoke the comprehensive data authorization process which basically discovers EntityIds mentioned in the request and authorizes each of such Ids.
Activity authorization events
As of this writing, there are two events invoked for activity authorization – OnActivityAuthorizing and OnActivityAuthorized. Neither of these events will be invoked if the requested operation is marked with RequestFeature.ActivityAuthorizationNotRequired. OnActivityAuthorizing is invoked just before activity authorization is performed. While OnActivityAuthorized is invoked if activity authorization succeeds (current identity is determined as permitted for the requested operation). You can do any data processing inside these events and can even throw an AuthFailedException to terminate the pipeline with an appropriate failure reason.
Handling authorization failures
If the authorization fails with a known failure, IAuthorizationProvider.HandleUnauthorizedRequest is called by the pipeline to process failure. The default implementation handles the failure as described in above error handling section.
Successful authorization event
Upon success, the ISecurityEvents.OnAuthorized method is invoked. This is where you can do post authorization processing or can even throw an AuthFailedException if you consider current identity as unauthorized.