Make secure Web API for Platform 3.0

There are so many aspects of security in virto platform and web API that the topic we’ll focus on authentication, authorization and some best practices that would be nice to know.

It has been confusing to differentiate between authentication and authorization. In fact, it is very simple.

  • Authentication: Refers to verify who you are, so you need to use username and password for authentication.
  • Authorization: Refers to what you can do, for example, access, edit or delete permissions to some documents, and this happens after verification passes.

Authenticate with ASP.NET Core Identity

Virto platform uses ASP.NET Core Identity as a membership system, using ASP.NET Core Identity enables several scenarios:

  • Create new user information using the UserManager type (userManager.CreateAsync).

  • Authenticate users using the SignInManager type. You can use signInManager.SignInAsync to sign in directly, or signInManager.PasswordSignInAsync to confirm the user’s password is correct and then sign them in.

  • Identify a user based on information stored in a cookie or barrier token so that requests from a browser will include a signed-in user’s identity and claims.

Issuing JWT tokens with OpenIddict

To enable token authentication, ASP.NET Core supports several options for using OAuth 2.0 and OpenID Connect. We take advantage of some good third-party library and use OpenIddict provides a simple and easy-to-use solution to implement an OpenID Connect server in platform application.

OpenIddict is based on AspNet.Security.OpenIdConnect.Server (ASOS) to control the OpenID Connect authentication flow and can be used with any membership stack, including ASP.NET Core Identity. Also, it supports various token formats https://openiddict.github.io/openiddict-documentation/guide/token-formats.html but in Virto platform, we use only JWT token for authorization, because of the following advantages:

  • Stateless – The token contains all the information to identify the user, eliminating the need for session state.
  • Reusability – A number of separate servers, running on multiple platforms and domains can reuse the same token for authenticating the user. It is easy to build an application that shares permissions with other applications.
  • JWT Security – No cookies so no need to protect against cross-site request forgery attacks (CSRF).
  • Performance – no server-side lookup to find and deserialize the session on each request, only need to calculate the HMAC SHA-256 to validate the token and parse its content.

Adding an OpenID Connect server to the platform allows us to support token authentication. It also allows you to manage all your users using a local password or an external identity provider (e.g. Azure Active Directory) for all your applications in one central place, with the power to control who can access your API and the information that is exposed to each client.

The stack of technologies on OpenIddict is built
image

Virto platform uses a JWT token authentication and use OAuth2 Password, Client Credentials and Refresh token flows to issue and consume authorization token for clients (see https://openiddict.github.io/openiddict-documentation/guide/token-formats.html)

Consume security tokens

The Virto platform manager SPA has a built-in implementation of JWT bearer token authorization. And has implementation for storing, refreshing and adding “authorization” header to each request to platform API. The platform manager application has the special AngularJS $http interceptor that performed all these tasks.
image

The platform itself configured to accept the issued JWT tokens and configured in accordance with this guide.
https://openiddict.github.io/openiddict-documentation/configuration/token-setup-and-validation.html#resource-server

Why we still need to use cookie-based authentication along with JWT in manager

Along with JWT token Virto manager also still uses the cookie-based authentication, this additional check is necessary due to impossibility intercept and inject Authorization header with bearer token for all API calls that are called not through the $http service. These calls can be produced by other third-party JS components and direct http links and cookie-based authorization is used to solve this problem.

When the user authorized in the platform, the system is intersected all user permissions with permissions described in Authorization: LimitedCookiePermissions and add them into cookies along with issue JWT token, and when the user makes a request to the platform, they are challenged against the helper cookie and the authentication token by following rules:

Recieved with request JWT Token Cookies JWT Token + Cookies
Is used for auth Token Cookie Cookies

You can configure which permissions can be stored in “limited_permissions" cookies by change this setting Authorization: LimitedCookiePermissions

appsetings.json

Configure multiple platform instances with shared token authentication

In some deployment scenarios when they are running multiple platforms instances one of them usually plays an authentication server role and has access to user accounts storages.
Other platform instances are play role as resource servers that simply need to limit access to those users who have valid security tokens that were provided by an authentication server.

image

Once the token is issued and signed by the authentication server, no database communication is required to verify the token.
Any service that accepts the token will just validate the digital signature of the token.

For that scenario, authentication middleware that handles JWT tokens is available in the Microsoft.AspNetCore.Authentication.JwtBearer package.
JWT stands for “JSON Web Token” and is a common security token format (defined by RFC 7519) for communicating security claims

The Virto platform has some settings that can be used to configure a resource server to consume to consume such tokens

appsettings.json

...
 "Auth": {
        //Is the address of the token-issuing authentication server.
        //The JWT bearer authentication middleware uses this URI to get the public key that can be used to validate the token's signature.
        //The middleware also confirms that the iss parameter in the token matches this URI.
        "Authority": "https://authentication-server-url",
        //represents the receiver of the incoming token or the resource that the token grants access to.
        //If the value specified in this parameter does not match the parameter in the token,
        //the token will be rejected.
        "Audience": "resource_server",

        "PublicCertPath": "./Certificates/virtocommerce.crt",
        "PrivateKeyPath": "./Certificates/virtocommerce.pfx",
        "PrivateKeyPassword": "virto"
    }
 ...

Authorization

After authentication, ASP.NET Core Web APIs need to authorize access. This process allows a service to make APIs available to some authenticated users, but not to all. Authorization can be done based on users’ roles or based on custom policy, which might include inspecting claims or other heuristics.

Restricting access to an ASP.NET Core MVC route is as easy as applying an Authorize attribute to the action method (or to the controller’s class if all the controller’s actions require authorization), as shown in the following example

By default, adding an Authorize attribute without parameters will limit access to authenticated users for that controller or action. To further restrict an API to be available for only specific users, the attribute can be expanded to specify required roles or policies that users must satisfy.

Permission-based Authorization

Typically, applications require more than just authenticated users. We would like to have users with different sets of permissions. The easiest way to achieve this is with the role-based authorization where we allow users to perform certain actions depending on their membership in a role.

For small applications, it might be perfectly fine to use role-based authorization, but this has some drawbacks. For instance, it would be difficult to add or remove roles, because we would have to check every AuthorizeAttribute with role specified in our code whenever we changed roles.

More flexible authorization could be implemented using claims. Instead of checking role membership, we check if a user has permission to perform a certain action. Permission, in this case, is represented as a claim.

In order to make it easier to manage claims, we can group them in roles. The latest versions of ASP.NET Core make this possible with role claims.

This solution has the following benefits:

  • It allows us to add/remove/delete roles without any changes in code.
  • We can re-define a role by changing its permissions.
  • Administration UI can be implemented to easily edit roles and permissions.

Permissions

In VC all permissions are defined on design time (from code) for each action grouped by a feature area. In this example, we are defining two feature areas with CRUD permissions. We are using constants because we will use these later in attributes, which require constant expressions.

You must register the permissions in the system in order to be able to use them in authorization checks and for role assignments in UI.

Use permissions for authorization

As the quantity of all permissions that can be defined is not determined, we need to create policies for those permissions. Under the hood, we automated this process by creating a custom policy provider PermissionAuthorizationPolicyProvider that dynamically creates a policy with the appropriate requirement as it’s needed during runtime.

After registration the policy provider in the Platform Startup.ConfigureServices

You can use the AuthorizeAttribute to check permission like this:

Resource-based (Scope-based) authorization

VC supports the “Resource-based” or another name “Scope-based” authorization is the authorization strategy depends upon the resource being accessed.

Consider an order that has a store property. Only orders belong to a particular store can be viewable for the user. Consequently, the order must be retrieved from the data store before authorization evaluation can occur.

Permission check based on [Authorize] attribute evaluation occurs before data binding and before the execution of the API action that loads the order. For these reasons, declarative authorization with an [Authorize] attribute doesn’t suffice. Instead, you can invoke a custom authorization method—a style known as imperative authorization.

Define the new permission-scope

Let’s see how the resource-based authorization works on one example:

We need to restrict user access to the only orders created in a particular store. In order to do this authorization check-in code and allow to assign this permission for end authorization role we must do the following steps:

Define the new OrderSelectedStoreScope class derived from PermissionScope, the object of this type will be used in role management UI and hold a store identifier selected by user and can be used for future authorization check.

Property StoreId will contain a store id of selected by the user on the role management UI.

To register the scope and make the “global” permission to have “scope-based” you need to add the following code in your Module.cs or Startup.cs file

Register the presentation template for “scope” is allows to user configure permission “scope” in the manager application.

Order.js

After these steps, all permissions that we have declared to be scoped” can be configured in role management UI and can be additionally restricted “bounded” with user data, in our case, it will be selected store.

Use resource-based authorization

Authorization is implemented as an IAuthorizationService service and is registered in the service collection within the Startup class. The service is made available via dependency injection to API controller actions.

In the following example, the CustomerOrderSearchCriteria to be secured an AuthorizeAsync overload is invoked to determine whether the current user is allowed to query the orders by the provided search criteria. To AuthorizeAsync are passed tree parameters

  • User – currently authenticated user with claims
  • Criteria – as an object that is secured and probably changed inside authorization handler in accordance with user restrictions
  • The new instance of OrderAuthorizationRequirement type with permission that needs to be checked

As a result, the authorization handler will check and change the criteria to return only orders with stores that the current users can view.

Write a resource-based authorization handler

Writing a handler for resource-based authorization isn’t much different than writing a plain requirements handler.

Inside this implementation, we load all StoreSelectedScope objects for user permissions (role claims)

And then use the store’s identifiers are retrieved from these scopes to change the CustomerOrderSearchCriteria in order to return only orders for stores that are enumerated in checking permission scopes.

Permissions localizations

Virto platform manager support localization resources for text, captions, tips etc. this is also true for permission names. This is achieved by adding into module localization resource file resources with a special key names

‘permissions:’ + permission.name

See example:

Authorization best practices

Do not open any API endpoints without authorization restrictions, especially endpoints that might provide access to customer sensitive data. These API must be always in “red zone” of security requirements.

Coming soon

Additional Resources

Understanding OAuth2
http://www.bubblecode.net/en/2016/01/22/understanding-oauth2/

ASP.NET Core Authentication
Introduction to Identity on ASP.NET Core | Microsoft Learn

Resource-Based Authentication

ASP.NET Core Authorization
Introduction to authorization in ASP.NET Core | Microsoft Learn

Role-based Authorization

Custom Policy-Based Authorization Policy-based authorization in ASP.NET Core | Microsoft Learn

2 Likes

Thanks for sharing. I find the article uniquely relevant, but hard to read. Some ideas for improving:

  • add Table of contents
  • define a clear structure (1., 2., 2.1, …)
  • split to several articles
  • prepare a high level, “light” version for this article
1 Like

great article. one remark - need to move the PermissionAuthorizationRequirement from Web to Data. it needs for extensions, for example when need to authorize in extension’s controllers. look at https://github.com/VirtoCommerce/vc-module-store/blob/release/3.0.0/src/VirtoCommerce.StoreModule.Data/Authorization/StoreAuthorizationRequirement.cs

1 Like