The Security Pipeline

Introduction

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.

Diagram depicting multi-stage ASPSecurityKit security pipeline performing various checks and verifications

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.

RequestService

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.

Request features

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.

Security context

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.

Error Handling

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:

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.

Security events

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.

Cross-site scripting

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.

Anonymous access

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

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.

Token evaluation

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:

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.

MFA applicability

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.

Verifying multi-factor

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

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.

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):

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.