How to Secure ASP.NET Core Applications with OpenIddict: Tech Case Study
Security is one of the core questions when building any websites and applications. A standard solution for automated authorization and access today is OAuth 2.0. There are many frameworks for ASP.NET Core apps to get secured this way, and one of them is OpenIddict. In this article, we will explain how OpenIddict works with the Virtocommerce platform, and provide real examples of integration and use.
At Virto Commerce platform, an example of asp net open source ecommerce, 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.
What is Openiddict
Openiddict is an open-source framework used to build servers in ASP.NET Core applications. It fully complies with OAuth 2.0 and OpenID Connect and even supports legacy ASP.NET apps.
OpenID Connect is a layer usually used by third-party apps to verify the end user's identity and get basic profile information before starting other processes. Basically, it means that the end user can use a single sign-on to access different related apps. OpenID Connect is totally free and can be used by anyone. It works in a transparent and clear way, just like OAuth:
- The user tries to enter the application based on OpenID.
- The system has client data from another OpenID-connected app, so it redirects the user to the provider to log in.
- The user only needs to enter the same password, and the verification is complete.
The architecture of the Virto Commerce B2B-First .NET Platform
Virto Commerce architecture was designed to help the development team focus on the implementation of business features without having to worry about CLEAN ARCHITECTURE.
- MICROSERVICES – The application can be composed by installing various modules, each for a particular service. This granular structure allows control by different teams and more efficient management.
- API-FIRST – eCommerce service has a true API design. All business logic is accessible via API using Rest or GraphQL language.
- CLOUD NATIVE – B2B eCommerce platform is delivered in a 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 of EXTENSIBILITY over CUSTOMIZATION in our product design on all layers.
- 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
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 the 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 the following:
- 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; you 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 the 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 as 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 to our solution.
Finally, we continued with OpenIddict since it satisfied all our requirements and was 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.
Kévin Chalet, author of OpenIddict, commented: "In 2020, ASOS was merged into OpenIddict 3.0 to form a unified stack under one library, which we no longer needed."
Technically, ASOS should work on even the most recent ASP.NET Core versions, but as mentioned here, 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.
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 of intercepting and injecting the Authorization header with a 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:
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. Implementing a refresh token flow with a session on your own is quite a challenging task and carries the risk of making unwise decisions that might introduce security vulnerabilities to the product.
Luckily, OpenIddict supports refresh token flow out of the box, naturally fitting into the existing Virto Commerce (VC) platform infrastructure. We didn’t do anything for this and used it as-is.
All requests that are passing from the VC-manager SPA application include authorization header with actual 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.
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.
The Shared Token Authentication Map
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.
As Kévin Chalet, author of OpenIddict, said, "we still have to use Microsoft.AspNetCore.Authentication.OpenIdConnect for communicating with OIDC provider."
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 with 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.
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.