ServiceStack Support
In this article
Features
All features of ASPSecurityKit are supported equally on ServiceStack. The base library – ASPSecurityKit.dll is agnostic of any web framework and is interface-driven, which means it relies on framework specific libraries to provide implementation of framework related services. A very good example of this is the IRequestService, which encapsulates functionality related to HTTP request/response. Since such functionality depends on the web framework services, IRequestService has no implementation in the base library; its ServiceStack impleentation can be found in RequestService.
Getting started
Learn important concepts and features of ASPSecurityKit for ServiceStack by following this tutorial in which you build a CRM API service from scratch using ASPSecurityKit’s ServiceStack project template.
Live Demo
Check out the live demo of the CRM service we built in this tutorial mentioned above. The source code for the same is available on GitHub.
Project Templates and Source Packages
ASK’s primary goal is to give you a convention based security framework but with flexibility and extensibility so you can replace any part of the framework if you so wish. Which ORM/data access technology one prefers is the question that may differ from one team/person to another and hence ASK goes on to declare even its security models like IUser, IPermit as interfaces, so you can define their implementation the way you want – including the validation, additional properties or even as data/view model.
However, to get you started quickly, a sample source code implementation of these components is provided through source packages.
The ServiceStack’s project template includes the most basic (Essential) of these source packages, which you can initialize as follows:
dotnet new -i ASPSecurityKit.Templates
dotnet new askss -n SuperCRM -o "D:\My Projects\SuperCRM"
ASK also provides a premium source package which additionally comes with source implementation of several commonly needed service operations, coded with best practices of async, managers, dependency injection and unobtrusive graceful error handling. This considerably speeds up developing the prototypes or initial version of the product services.
You can go through this walkthrough to get started with the Premium source package if you have already bought it, or can try it out before buying the same.
The security pipeline
The recommended way to protect ServiceStack’s requests with ASK’s Zero Trust security pipeline is by using the global async filter, which is the default when you add the ASPSecurityKitFeature plugin as follows:
appHost.Plugins.Add(new ASPSecurityKitFeature());
Alternatively, you can also use the ProtectAttribute on your service class (or a base service) to enable the protection for only a portion of the application. You’d still need to add the plugin but turn off the async global filter:
using ASPSecurityKit.ServiceStack;
[Protect]
public class ServiceBase : Service
{
...
}
However, the attribute based filters in ServiceStack do not support async execution and hence we recommend the other approach.
The Zero Trust nature of the pipeline means that all checks are considered applicable to every operation; you selectively disable one or more checks for one or more operations.
While individual attributes are available to denote which check (known as RequestFeature) you don’t need; however, you can also use Feature attribute, especially when you need to indicate multiple checks you don’t need for an operation.
In the following sections, we’ll go through various checks of the pipeline with examples but refrain from providing the conceptual overview and behind the scene technical information which are already covered comprehensively in the security pipeline article.
Authentication
Apply AllowAnonymous attribute on the DTO to allow unauthenticated request to the operation. If request carries a token, it’s validated and identity is initialized upon success. Check out Anonymous Access section for more information.
[DataContract]
[Route("/sign-up", "POST")]
[AllowAnonymous]
public class SignUp : IReturn<BaseRecordResponse<AppUserDetails>>
{
[DataMember]
public string Name { get; set; }
[DataMember]
public string Email { get; set; }
[DataMember]
public string Password { get; set; }
[DataMember]
public bool RememberMe { get; set; }
}
Built-in support exists for multiple kinds of tokens, for example:
- HMAC: Leverage this snippet to sign your requests with HMAC.
- For cookie based token, you can simply call login like so:
var result = await this.authSessionProvider.LoginAsync(request.Email, request.Password, false, this.SecuritySettings.LetSuspendedAuthenticate, true);
However, you need to enable the support for cookie explicitly – you can do so by decorating the base DTO with AcceptAuthCookie attribute.
In most cases, we recommend using HMAC tokens for APIs as HMAC provides better security and integrity guarantees and is easily implementable even in SPA clients. Certain relaxations like excluding body from HMAC computation are already available in HMAC scheme to handle such scenarios as uploading files, so there shouldnt' be a need for using cookie authentication with APIs.
- Use ServiceKey based token if you’re just looking for a simple key-based authentication wherein just attaching an APIKey to the request authenticates the caller. Do follow the best practices to reduce risks involved with the key-based authentication.
To learn more visit Authentication section on the pipeline page and Authentication Schemes page.
You can also check out building credential-based authentication step of the hands-on tutorial.
Activity Data Authorization (ADA)
Activity authorization is the process of authorizing operations (activities) by looking up associated permissions in the user’s permit set. It’s different from role-based authorization in a way that you don’t hard-code roles in code.
However, with ASK’s ADA, not just the activity but also the data sent as input to that activity (request) is also authorized; ADA traverses through the Request DTO, discovering key data (EntityId) fields and authorizing each of them.
(Reading the design guide and how-to docs is mandatory to understand the concepts and power of ADA).
You can also check out building multi-tenant contact management authorized using ADA step of the hands-on tutorial.
- If your Request DTOs have a suffix, specify the same to have it removed before determining the permission for a Request DTO.
var settings = funqContainer.Resolve<IServiceStackSecuritySettings>();
settings.DTOSuffix = "Request";
...
[DataContract]
[Route("/contacts", "POST")]
// The permission for this will be CreateContact after removing the suffix 'Request'
public class CreateContactRequest : Contact, IReturn<BaseRecordResponse<Contact>>
{
}
[DataContract]
[Route("/contacts/{ContactId}", "DELETE")]
// The permission for this will be DeleteContact after removing the suffix 'Request'
public class DeleteContactRequest : IReturn<BaseResponse>
{
[DataMember]
public Guid ContactId { get; set; }
}
- Use AuthPermission attribute to specify an arbitrary, custom permission for a DTO.
[DataContract]
[Route("/permissions", "GET")]
[AuthPermission(PermissionCodes.OrganizePermissions)]
public class GetPermissions : IReturn<BaseListResponse<Permission>>
{
}
[DataContract]
[Route("/permissions", "POST")]
[AuthPermission(PermissionCodes.OrganizePermissions)]
public class AddPermission : Permission, IReturn<BaseRecordResponse<Permission>>
{
}
- To update the rule that discovers key data field in the DTO across all operations within the app, override the pattern.
// Adding "number" additionally to identify fields like AccountNumber as identifier to be authorized.
ASPSecurityKit.Authorization.EntityIdAuthorizer.IdMemberSelectorRegexPattern = "^.*?(Id|((?<=^)id)|(?i:username|urn|userid|entityid|number))$"
- To authorize a particular key field – having non-conventional name – without updating the default pattern, use Authorize attribute. On the other hand, use DoNotAuthorize attribute to ignore a particular field from being identified as a key field even though it’s name matches the pattern.
[DataContract]
[Route("/accounts", "POST")]
public class CreateAccount : IReturn<BaseRecordResponse<Account>>
{
[DataMember]
// A bank institution can create an account for any customer user within
// the system and no bank institution owns any customer and hence we don't
// need to authorize this field.
[DoNotAuthorize]
public string Username { get; set; }
[DataMember]
public double Amount { get; set; }
[DataMember]
public Guid? BranchId { get; set; }
[DataMember]
// It's a publically accessible configuration option for account types
// so no need for authorization to use as reference in account entity.
[DoNotAuthorize]
public Guid AccountTypeId { get; set; }
}
[DataContract]
[Route("/transfers", "POST")]
public class Transfer : IReturn<BaseRecordResponse<Transfer>>
{
[DataMember]
// Though this has 'Id' as suffix and will be matched
// by the default pattern, we're using the Authorize attribute
// to override the member name to locate the related references
// loader which is written for AccountId.
[Authorize("AccountId")]
public Guid FromAccountId { get; set; }
[DataMember]
// This serves two purpose:
// 1. The default pattern doesn't recognize such names as an identifier.
// If you haven't overridden the same, you can instead indicate
// the same intent with Authorize attribute.
// 2. With the param 'AccountNumber' name,
// you're telling the name by which the related references loader
// to be located for this identifier.
[Authorize("AccountNumber")]
public string ToAccountNumber { get; set; }
[DataMember]
public double Amount { get; set; }
[DataMember]
public string Remarks { get; set; }
}
- You can also write a complete custom logic to authorize requests for a particular operation, overriding the default, automatic authorization performed by ADA.
- To manually perform authorization against the in-memory permit set, call one of the IsAuthorized overloads on UserService, while to manually perform authorization leveraging the related references feature instead, call one of the
IsAuthorized
overloads of EntityIdAuthorizer (it also eventually uses UserService.IsAuthorized after loading the related references for each EntityId being authorized).
public class PermitsAuthDefinition : AuthDefinitionBase
{
public PermitsAuthDefinition(AppDbContext dbContext, IEntityIdAuthorizer entityIdAuthorizer)
: base(dbContext, entityIdAuthorizer)
{
}
public async Task<AuthResult> IsAuthorizedAsync(GrantUserPermit dto, string permissionCode)
{
var result = await this.entityIdAuthorizer.IsAuthorizedAsync(permissionCode,
new { UserPermitId = dto.Id, dto.UserId, dto.UserPermitGroupId });
if (result.IsSuccess)
{
return await this.entityIdAuthorizer.IsAuthorizedAsync(dto.PermissionCode, new { dto.EntityId });
}
return result;
}
}
- To opt an operation out of ADA check, including the entity suspension, use SkipActivityAuthorization attribute.
Multi-Factor Authentication (MFA)
- If you don’t need MFA, you need not do anything as until it’s enabled on user, it has no impact.
- To opt an operation out of MFA check, decorate the DTO with MFANotRequired attribute (or its equivalent RequestFeature value).
[DataContract]
[Route("/self/send-twofactor-authentication-token", "POST")]
// Both MFA and authorization steps aren't required to send MFA token.
[Feature(ApplyTo.Post, RequestFeature.AuthorizationNotRequired, RequestFeature.MFANotRequired)]
public class SendTwoFactorAuthenticationToken : IReturn<BaseResponse>
{
[DataMember]
public bool ResendToken { get; set; }
}
- For an operation that is involved in setting up MFA, you should decorate it with MultiFactorSetting attribute (or its equivalent RequestFeature value). This is because the MFA settings operation shouldn’t be accessible when user is prompted for MFA, but we have a feature called MFA enforcement, which helps you in forcing users to enable MFA before they can proceed with the account – typically needed in enterprise scenarios to harden security of employee accounts.
[DataContract]
[Route("/self/twofactor-settings", "PATCH")]
[Feature(ApplyTo.Patch, RequestFeature.MFASetting, RequestFeature.ActivityAuthorizationNotRequired)]
public class ChangeTwoFactorAuthSettings : IReturn<BaseResponse>
{
[DataMember]
public bool? Enabled { get; set; }
}
- Premium source package has implementation of MFA workflow – including the required operations – based on email based MFA token delivery.
Learn more in the MFA section on the pipeline page.
Verification
Typically every multi-user web app needs to verify the identity of the user to safely communicate with the user post sign up. It may involve verifying the email or phone number. ASK’s user verification check helps in enforcing such a requirement.
- If you don’t need verification for the app, set MustHaveBeenVerified setting to
false
.
var settings = funqContainer.Resolve<IServiceStackSecuritySettings>();
settings.MustHaveBeenVerified = false;
- To opt an operation out of verification check, decorate the DTO with VerificationNotRequired attribute.
[DataContract]
[Route("/self/verification-email", "POST")]
[SkipActivityAuthorization]
[VerificationNotRequired]
public class ResendVerificationEmail : IReturn<BaseResponse>
{
}
- Both Starter and Premium source packages have implementation of email verification workflow.
Learn more in the Authorization section on the pipeline page.
You can also check out building email verification step of the hands-on tutorial.
Cross-Site Scripting (XSS)
XSS validation of input data is enabled by default; to disable it for the whole app, turn off ValidateXss property during plugin setup:
appHost.Plugins.Add(new ASPSecurityKitFeature { ValidateXss = false });
You can also use AllowHtml attribute on specific properties to skip XSS validaition on them.
Learn more in the XSS section on the pipeline page, and also go through this guide to learn about measures beyond input validation that you need for comprehensive XSS mitigation.
Other Checks
Similarly, for checks like password expiration, IP firewall, impersonation, suspension etc. we have either attributes or settings to customize the default behavior.