How to Secure ASP.NET Core Applications with OpenIddict Using Virto Commerce B2B eCommerce: Tech Case Study

In Virto Commerce platform, we take advantage of our good third-party library and use OpenIddict, which provides a simple and easy-to-use solution to implement an OpenID Connect server to access RESTful endpoints from Single Page Applications (SPAs), native clients, using bearer token authentication. These types of applications do not work with cookies, but can easily retrieve a bearer token and include it in the authorization header of subsequent requests.

To enable token authentication, ASP.NET Core supports several options for using OAuth 2.0 and OpenID Connect. In more detail, ASP.NET Core comes with two built-in OAuth 2.0 and OpenID Connect client handlers (to act as a relying party) and also offers a JWT bearer authentication handler for token validation, but nothing to act as an OAuth 2.0/OIDC server (i.e., an identity provider). However, they aren't sufficient for cases when you need to issue security tokens for local ASP.NET Core Identity users rather than using an external identity provider, which the Virto Commerce platform actually does.

Customer Profile

Virto Commerce is an open-source platform for building extensible ecommerce applications. We provide complex digital commerce solutions for B2B, B2C, and B2B2C businesses, marketplaces, and delivery of the platform as cloud-ready or SaaS.

Virto Commerce platform architecture

Virto Commerce architecture was designed to help the development team focus on the implementation of business features without having to worry about CLEAN ARCHITECTURE.

  • MODULAR – Application can be composed by installing various modules, which can be developed by different teams and have individual release cycles.
  • API-FIRST – eCommerce service has true API design. All business logic is accessible via API using Rest or GraphQL language.
  • CLOUD NATIVEB2B eCommerce platform is delivered in the cloud-ready Azure-centric manner and has out-of-the-box integration with Azure services.
  • HEADLESS – This architecture allows an enterprise to support omnichannel journeys across traditional and digital touchpoints as well as new business models.
  • EXTENSIBILITY – With API-first and persistence models, business logic can be extended as needed without customization and redeploying the solution. This provides superior business agility and keeps us up to date. We follow the principle EXTENSIBILITY over CUSTOMIZATION in our product design on all layers.

Technology Stack

  • ASP.NET Core 3.1.0 as the platform framework
  • EF Core 3.1.0 as primary ORM
  • ASP.NET Core Identity 3.1.0 for membership
  • OpenIddict 3.0.0 for OAuth authentication and authorization
  • WebPack as primary js and CSS design/runtime bundler and minifier
  • Swashbuckle.AspNetCore.SwaggerGen for Swagger docs and UI
  • SignalR Core for push notifications
  • AngularJS 1.4 as the primary framework for SPA (we are working on a replacement to Vue 3.0)
  • HangFire 1.7.8 for run background tasks

Problem Statement

In 2019, as part of the update project from ASP.NET (.NET 4.6) to ASP.NET Core (.NET Core), the Virto Commerce team faced the challenge of replacement for AspNet.Security.OpenIdConnect.Server (ASOS). It is a low-level OpenID Connect OWIN/Katana that had been used for securing and token authentication for Virto Commerce Web API before that was included as part of ASP.NET Core and worked on even the most recent versions.

We chose JWT token for authentication for the Virto Commerce platform RESTful endpoints, because of the following advantages:

  • Stateless – The token contains all the information to identify the user, eliminating the need for a session state.

We rejected some stateful (session) tokens that OpenIddict provides in the early version based on:

  • Reusability – Separate servers running on multiple platforms and domains can reuse the same token for authenticating the user. It is easiest to build an application that shares permissions with other applications.
  • JWT Security – No cookies are required to protect against cross-site request forgery attacks (CSRF).
    It is necessary to add the following about the format of tokens – if you can avoid adding CSRF/anti-forgery countermeasures when using tokens, it's not related to the format they use (mentioned as JWT above), but it's due to the fact that they are not automatically attached to every request sent to a domain by browsers like cookies (they are typically attached to the Authorization header by the client app).
  • 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.

Here are the main requirements by which we were guided:

  • Issue and consume JWT tokens for authentication;
  • Consume JWT tokens, which are issued by external authorization servers, since Virto Commerce platform solution can be hosted as a set of independent, shared-to-nothing services (microservices architecture);
  • Support OpenIdConnect and OAuth 2.0 non-interactive flows (password and client credentials flows, with refresh tokens);
  • Native integration with ASP.NET Core and local user information storages (user provisioning) with ASP.NET Core Identity; and
  • SSO with Identity Access Management systems such Azure AD.

There were only two open-source projects at the moment, including IdentityServer and OpenIddict, but we rejected IdentityServer since it hadn't adopted well to .NET Core. Also, OpenIddict is a little bit more low-level than IdentityServer. IdentityServer gives you a running solution out-of-the-box, whereas for OpenIddict to work, you need to implement some details yourself. It was an ideal solution for us at that moment, because “less is more” and we didn’t want to introduce extra complexity into our solution.

Finally, we continued with OpenIddict since it satisfied all our requirements and natively integrated with the ASP.NET Core Identity membership system. As it turned out, in the future, the creators/maintainers of IdentityServer decided to dual-license future versions of IdentityServer. Now, unless you are working on an open-source project, you will have to pay for a commercial license, so we did not make a wrong choice (as it seems to us).

An example would be a recent blog post about IdentityServer alternatives, which they called "bare metal"; see this here:

openiddict technology stack

In 2020, ASOS was merged into OpenIddict 3.0 to form a unified stack under one library, which we no longer needed

commented Kévin Chalet, author of OpenIddict

Technically, ASOS should work on even the most recent ASP.NET Core versions, but as mentioned in, its deprecation was motivated by two aspects:

1) ASOS, the aspnet-contrib token authentication handlers and OpenIddict were in three separate repositories and it was time-consuming to keep the three repositories in sync when introducing changes. Moving to a single product/monorepo approach greatly simplified the development story.

2) ASOS actually had two flavors: an ASP.NET Core version and an OWIN/Katana one. While very similar, the two versions didn't share much code. Merging ASOS into OpenIddict and making OpenIddict host-agnostic allowed introducing native OWIN/Katana support in OpenIddict without having to keep two separate but similar packages like ASOS. In OpenIddict 3.x, there's now a main "server" package and two ASP.NET Core/OWIN hosts that only contain the code needed to integrate OpenIddict with ASP.NET Core or OWIN. It's a much more efficient approach.

The Solution, Steps and Delivery

Because the desired requirements were to be implemented in different ways, we divided our work into four parts:

  • OpenIddict integration into existing ASP.NET Core and Identity membership to issue and consume JWT tokens with refresh token flow support.
  • Add client credentials flow to the application to generate and store access data for machine-to-machine authentication.
  • Configure multiple platform instances with a shared authentication token.
  • SSO authentication with IAM (such as Azure AD) for users managing the platform.

In most cases, the Virto Commerce platform plays the Authorization and Resource server roles at the same physical instance, so we must include the extra configuration into the application to configure the resulting application depending on its role.

Issue JWT Tokens with Password Grand Credentials Flow

For this, we followed the default recommendations on how to set up the OpenIddict server in the ASP.NET Core application. We started with the Password, Client Credentials Flow with the enabled refresh token flow.

In the Virto Commerce platform, we intentionally disabled authorization grand flow support, since we have no plans to use Virto Commerce as an Authorization Server for SSO yet.

Here is our Startup.cs (link to code).

See code

                                    //With these lines we load authorization settings from configuration
var authorizationOptions = Configuration.GetSection("Authorization").Get<Platform.Core.Security.AuthorizationOptions>(); 
// Register the OpenIddict services.
// Note: use the generic overload if you need
// to replace the default OpenIddict entities.
.AddCore(options => 
//Use EF integration pass to OpenIddict tha database is used by ASP.NET Identity stores
}).AddServer(options => 
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
var builder = options.UseAspNetCore(). 
// Enable the authorization, logout, token and userinfo endpoints.
//Set the lifetime for tokens are loaded from configuration
// When request caching is enabled, authorization and logout requests
// are stored in the distributed cache by OpenIddict and the user agent
// is redirected to the same page with a single parameter (request_id).
// This allows flowing large OpenID Connect requests even when using
// an external authentication provider like Google, Facebook or Twitter.
//We don't use scopes validation, instead we use internal permissions-based authroization, where each permission is stored as user claims and included in the JWT token that will be used later for authorizations checks
// During development or when you explicitly run the platform in production mode without https, need to disable the HTTPS requirement.
if (WebHostEnvironment.IsDevelopment() || !Configuration.IsHttpsServerUrlSet()) 
// Note: to use JWT access tokens instead of the default
// encrypted format, the following lines are required:
var bytes = File.ReadAllBytes(Configuration["Auth:PrivateKeyPath"]); 
X509Certificate2 privateKey; 
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 
// macOS cannot load certificate private keys without a keychain object, which requires writing to disk. Keychains are created automatically for PFX loading, and are deleted when no longer in use. Since the X509KeyStorageFlags.EphemeralKeySet option means that the private key should not be written to disk, asserting that flag on macOS results in a PlatformNotSupportedException.
privateKey = new X509Certificate2(bytes, Configuration["Auth:PrivateKeyPassword"], X509KeyStorageFlags.MachineKeySet); 
privateKey = new X509Certificate2(bytes, Configuration["Auth:PrivateKeyPassword"], X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.EphemeralKeySet); 

Because our configuration contains some differences from the original OpenIddict sample

openiddict-samples/samples/Hollastin/Hollastin.Server/Startup.cs, we want to explain some of them:

  • We read lifetime settings for tokens: RefreshTokenLifeTime and AccessTokenLifeTime from application configuration to be able to change these values from the environment.
  • Options.DisableScopeValidation() - We do not intend to use our platform to check JWT tokens issued by other Authorization services so far (accepting only tokens issued by another Virto Commerce, for instance). In addition, we do internal permissions-based authorization, where each permission is stored as user claims and included in the JWT token that will be used for further authorization checks at the application level.
    In this context, scopes are considered "client application permissions" that allow a user or an identity provider to limit the things a client can do when interacting with an API; these don’t play an important role in our application and we don’t use them.
  • We do not use dev certificates and use embedded into platform default X.509 certificate (private key) for token encryption that must be replaced on a new one for each new installation on production. (It is for simplification of the shared token across multiple instances configuration).

For the next step, we add the controller that handles ~/connect/token requests and issues JWT tokens for the clients.

AuthorizationController.cs (link to original code)

See code

                                    [HttpPost("~/connect/token"), Produces("application/json")] 
public async Task<ActionResult> Exchange() 
OpenIddictRequest openIdConnectRequest = HttpContext.GetOpenIddictServerRequest(); 
if (openIdConnectRequest.IsPasswordGrantType()) 
var user = await _userManager.FindByNameAsync(openIdConnectRequest.Username); 
if (user == null) 
 var properties = new AuthenticationProperties(new Dictionary<stringstring>
                        [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                        [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid."
// Validate the username/password parameters and ensure the account is not locked out.
var result = await _signInManager.CheckPasswordSignInAsync(user, openIdConnectRequest.Password, lockoutOnFailure: true); 
if (!result.Succeeded) 
 var properties = new AuthenticationProperties(new Dictionary<stringstring>
                        [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                        [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid."
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(openIdConnectRequest, user); 
var claimsPrincipal = await _userClaimsPrincipalFactory.CreateAsync(user); 
//Do not allow login to customers
if (claimsPrincipal.Claims.Any(x => x.Type == _identityOptions.Value.ClaimsIdentity.RoleClaimType && x.Value == PlatformConstants.Security.SystemRoles.Customer)) 
 var properties = new AuthenticationProperties(new Dictionary<stringstring>
                        [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                        [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not allowed to sign in."
                    return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
var limitedPermissions = _authorizationOptions.LimitedCookiePermissions?.Split(PlatformConstants.Security.Claims.PermissionClaimTypeDelimiter, StringSplitOptions.RemoveEmptyEntries) ?? new string[0]; 
if (!user.IsAdministrator) 
limitedPermissions = claimsPrincipal 
.Where(c => c.Type == PlatformConstants.Security.Claims.PermissionClaimType) 
.Select(c => c.Value) 
.Intersect(limitedPermissions, StringComparer.OrdinalIgnoreCase) 
if (limitedPermissions.Any()) 
// Set limited permissions and authenticate user with combined mode Cookies + Bearer.
// LimitedPermissions claims that will be granted to the user by cookies when bearer token authentication is enabled.
// This can help to authorize the user for direct(non - AJAX) GET requests to the VC platform API and / or to use some 3rd - party web applications for the VC platform(like Hangfire dashboard).
// If the user identity has a claim named "limited_permissions", this attribute should authorize only permissions listed in that claim. Any permissions that are required by this attribute but
// not listed in the claim should cause this method to return false. However, if permission limits of user identity are not defined ("limited_permissions" claim is missing),
// then no limitations should be applied to the permissions.
((ClaimsIdentity)claimsPrincipal.Identity).AddClaim(new Claim(PlatformConstants.Security.Claims.LimitedPermissionsClaimType, string.Join(PlatformConstants.Security.Claims.PermissionClaimTypeDelimiter, limitedPermissions))); 
await HttpContext.SignInAsync(IdentityConstants.ApplicationScheme, claimsPrincipal); 
await _eventPublisher.Publish(new UserLoginEvent(user)); 
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); 

This code is almost identical to the original sample:, but contains some code that stores a limited set of user permissions in the user cookies session in order to implement Hybrid (Cookies + Bearer) authorization.

Why Do We Still Need to Use Cookie-Based Authentication Along with JWT in Manager (SPA)?

Along with a JWT token, Virto manager also still uses cookie-based authentication. This additional check is necessary due to the impossibility to 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; direct http links and cookie-based authorization are used to solve this problem.

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

Received with request
JWT Token
JWT Token + Cookies

Is used for auth




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

VirtoCommerce.Platform.Web/appsettings.json (original code link here)

See code

                                    "Authorization": {

Refresh Token Flow

Since the access token issued by an application has a limited lifetime, it’s important to provide a good user experience and not push the user to the login form every time. The user has to authenticate only once, through the web authentication process. Subsequent reauthentication can take place without user interaction, using the refresh token. The refresh token is a special kind of token used to obtain a renewed access token. To implement refresh token flow with session by your own is quite a challenging task, and carries the risk of making unwise decisions that might introduce security vulnerabilities into the product.

Luckily, OpenIddict supports refresh token flow out of the box, naturally fitting into existing Virto Commerce (VC) platform infrastructure. We didn’t do anything for this and use it as-is.

AngularJs Workflow

Virto Commerce platform manager SPA has a built-in implementation of JWT bearer token authorization. It has capabilities for storing, refreshing, and adding an “authorization” header to each request to the platform API. Also, it is stored in local storage as the JWT token is refreshed. The platform manager application has the special AngularJS $http interceptor that performs all these tasks.

Here is an example pseudo js, using the http service from Angular and passing the user credentials during an http request with refresh tokens:

See code

                                    app.factory('BearerAuthInterceptor', function ($window, $q) { 
return { 
request: function (config) { 
config.headers = config.headers || {}; 
return extractAuthData() 
.then(function (authData) { 
if (authData) { 
config.headers.Authorization = 'Bearer ' + authData.token; 
return config; 
response: function(response) { 
if (response.status === 401) { 
// Redirect user to login page / signup Page.return response || $q.when(response); 
function extractAuthData() { 
var authData = $window.localStorage.getItem('auth_data').getStoredData(); 
if ( < authData.expiresAt) { 
return $q.resolve(authData); 
var data = 'grant_type=refresh_token&refresh_token=' + encodeURIComponent(authData.refreshToken);
return $'connect/token', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).then( 
//store new access_token and refresh_token in the local storage. Rest of code skip for clarity
// Register the previously created AuthInterceptor.
app.config(function ($httpProvider) { 

All requests that are passing from the VC-manager SPA application include authorization header with actual JWT token value:

jwt token value

Add Client Credentials Flow for Machine-to-Machine Authentication

Virto Commerce platform authenticates and authorizes the app rather than a user. For this scenario, typical authentication schemes like username + password or social logins don't make sense.

Instead, Virto Commerce apps use the Client Credentials Flow (defined in OAuth 2.0 RFC 6749, section 4.4), in which it passes along their Client ID and Client Secret for authentication to get a token.

OpenIddict provides native support of Client Credentials Flow along with API and storage to manage registered applications and authorize the clients by stored ClientId & ClientSecret pair. All we need to do is make our own UI.

Virto Commerce platform

We didn't implement scopes for applications completely, and each application acts as an administrator. In future plans, we intend to use roles and permissions as custom scopes to limit access to resources according to granted permissions (scopes) for an application.

AuthorizationController.cs (link to original code)

See code

                                    [HttpPost("~/connect/token"), Produces("application/json")] 
public async Task<ActionResult> Exchange() 
else if (openIdConnectRequest.IsClientCredentialsGrantType()) 
// Note: the client credentials are automatically validated by OpenIddict:
// if client_id or client_secret are invalid, this action won't be invoked.
var application = await _applicationManager.FindByClientIdAsync(openIdConnectRequest.ClientId, HttpContext.RequestAborted); 
if (application == null) 
  var properties = new AuthenticationProperties(new Dictionary<stringstring>
                        [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidClient,
                        [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The client application was not found in the database."
  return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// Create a new authentication ticket.
var ticket = CreateTicket(application); 
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); 
private AuthenticationTicket CreateTicket(OpenIddictEntityFrameworkCoreApplication application) 
// Create a new ClaimsIdentity containing the claims that
// will be used to create an id_token, a token or a code.
var identity = new ClaimsIdentity( 
// all clients act as administrator
var principal = new ClaimsPrincipal(identity);
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket( 
new AuthenticationProperties(), 
return ticket; 

Configure Multiple Platform Instances with Shared Token Authentication

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

token authentication

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 this 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.

Here is a code in Startup.cs that tells the Virto Commerce platform application that works as a resource server to accept a JWT token issued by an authorization server (which is a Virto Commerce platform application also). It is accessible by URL specified in Configuration["Auth:Authority"] and verifies the signature use of the RSA public key that is embedded into an application from this path Configuration["Auth:PublicCertPath"].

Startup.cs (link to original code)

See code

                                    authBuilder.AddJwtBearer(options => 
options.Authority = Configuration["Auth:Authority"]; 
options.Audience = Configuration["Auth:Audience"]; 
if (WebHostEnvironment.IsDevelopment()) 
options.RequireHttpsMetadata = false; 
options.IncludeErrorDetails = true; 
X509SecurityKey publicKey = nullif (!Configuration["Auth:PublicCertPath"].IsNullOrEmpty()) 
var publicCert = new X509Certificate2(Configuration["Auth:PublicCertPath"]); 
publicKey = new X509SecurityKey(publicCert); 
options.TokenValidationParameters = new TokenValidationParameters() 
NameClaimType = OpenIddictConstants.Claims.Subject, 
RoleClaimType = OpenIddictConstants.Claims.Role, 
ValidateIssuer = !string.IsNullOrEmpty(options.Authority), 
ValidateIssuerSigningKey = true, 
IssuerSigningKey = publicKey 

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


See code

"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.

Because of custom claims and OpenID Connect protocol offers, we can do any authorization check on Resource servers without access to the user data and work autonomously.

SSO with Azure Active Directory

By default, Virto Commerce platform provisions the user data but, in some scenarios, it is required to allow users of a given organization to sign in or sign up using their Azure Active Directory (AD) account.

In the platform, we added SSO with Azure AD, but it has had some limitations. Azure AD is used here only for authentication and doesn't involve any authorization check for platform resources.

After the initial "sign in" in Virto Commerce using an Azure AD account, we create a new user account automatically in the internal membership storage (transparent sign up) and associate it with the external AD. Afterward, the administrator could assign any roles and permissions to this newly-created account to be able to control access to platform resources. No AD authorization data, including built-in or custom, will take this into account.

Startup.cs (link to original code)

See code

                                    var options = new AzureAdOptions(); 
if (options.Enabled) 
authBuilder.AddOpenIdConnect(options.AuthenticationType, options.AuthenticationCaption, 
openIdConnectOptions => 
openIdConnectOptions.ClientId = options.ApplicationId; 
openIdConnectOptions.Authority = $"{options.AzureAdInstance}{options.TenantId}"; 
openIdConnectOptions.UseTokenLifetime = true; 
openIdConnectOptions.RequireHttpsMetadata = false; 
openIdConnectOptions.SignInScheme = IdentityConstants.ExternalScheme; 
openIdConnectOptions.SecurityTokenValidator = defaultTokenHandler; 
openIdConnectOptions.MetadataAddress = options.MetadataAddress; 
sign in with azure active directory

We still have to use Microsoft.AspNetCore.Authentication.OpenIdConnect for communicating with OIDC provider

commented Kévin Chalet, author of OpenIddict

OpenIddict only provides the server and token validation parts (though we plan to create a client in the future, as I'm not 100% satisfied by existing options), so if you need to communicate with an external OIDC provider, you'll still need Microsoft.AspNetCore.Authentication.OpenIdConnect. Using OpenIddict as a proxy between your clients and an external provider is also possible, but you'll want to implement an interactive flow like the "code flow" to support this scenario.

Future Directions

Overall, we are quite happy with how we eliminated much of the work related to authorization and authentication tasks in our project. This is thanks to OpenIddict’s great integration with ASP.NET Core services and comprehensive documentation and sampling, along with the support of the author and soul of this project, Kévin, because of whom we could focus on other things during our development, and tackle such complex subjects as security in the easiest way.

Request a quick demo

Eugeney Tatarincev
Platform Architect