While developing an enterprise app for one of our clients, we encountered a rather interesting requirement requested by the end users – to reduce the number of user accounts required. The current setup had an authorization endpoint within the application which was used to authenticate from several client apps. However, individuals within the company using that enterprise solution needed to have, in addition to their own corporate account, a separate account for our app, and yet another account to access a ticketing system (Jira). Not only does this have a negative impact on user experience, it also adds an extra administrative layer of creating accounts as well as password management and rotation for each of them. When planning a single sign-on implementation with reliability, scalability and high customization in mind, the choice will usually be Duende Identity Server (former Identity Server 4) or Okta. Okta comes with more administrative tools and is cloud-based, whereas Duende Identity Server can be deployed wherever you desire, is open-source and, based on your needs, could come with a smaller price tag than Okta.
We have two main goals here – one is to create a single identity provider for several systems (in this scenario, our app and a ticketing system; it could also be multiple apps within an application suite). The other is to integrate existing user accounts (their corporate accounts) with our identity provider. The first part requires creating and deploying the identity server. The second, however, requires implementing integration with external identity providers such as Azure AD.
The base idea is fairly simple – our application’s user database remains the same and contains user information and privileges/access level pertinent to our application. Every client application that utilizes the identity server has its individual configuration. Here we can define the client itself (using a client ID and secret) as well as allowed grants and scopes, meaning we can limit which claims are sent to each individual client.
In order to create a simple demo (which can be downloaded from here), I have used the Duende Identity Server EF Core template as a starting point and opted for MS SQL for storing configuration and user data in order to keep everything within the Microsoft ecosystem. Tables containing two dummy users, with plaintext passwords (don’t do this) and a single role each (Administrator/User) (alice/alice and bob/bob) have been created to demonstrate authorization and custom claim generation.
The demo solution consists of the following
- Identity Server
- simple MVC client
MVC client and the API illustrate how to interact with the Identity Server to login and access token data, as well as display the generated access tokens.
Authentication and authorization
User authentication can be performed within the Identity Server (the Login POST method within the Account controller). A simple user store has been implemented here, but the process itself is straightforward – just validate the received user credentials and, if needed, specify additional properties to be added to the authentication cookie. Token generation, validation and refresh are being handled by the Identity Server.
Performing authentication against the application user database forces users to have separate user credentials for our app. The idea behind using an external provider is to perform the authentication against a trusted 3rd party. Duende Identity server comes with a built-in OIDC client, and this example utilizes demo.duendesoftware.com as the external identity provider (however, there are out-of-the-box solutions offering SAML support for both federating with SAML providers and using Identity Server as a SAML identity provider). A more common scenario would be using the company’s Azure AD. In addition to offloading the authentication process to a third-party, this also comes with all the bells and whistles of Azure AD, like allowing app authentication only if the user is authenticated using MFA, or using a pre-approved device. User provisioning and privilege assignment can be automated or partially automated by exposing AD group membership in the token (For example, if the user authentication was successful, and user is a member of OurApplicationUsers AD group, create a new account for them). This allows end customer for easier account administration and reduces the account management overhead.
The only remaining issue is linking the data received from the external provider with application users. Depending on the setup, there are two options here – using something like an e-mail or UPN for the username and returning that information from the Azure AD, or having a separate user attribute that corresponds to their external ID.
The easiest way to register external providers is statically, in the startup. The configuration is loaded once on application startup and is quick and easy solution for a „set it and forget it“ scenario. This comes with significant downsides, though – each time you add a new external provider, or just rotate the application secret, you need to restart the Identity Server (potentially disrupting access to several of your applications). Identity Server’s Enterprise license, on the other hand, supports dynamic loading of external providers by implementing your own identity provider store. The great added benefit here is the ability to store sensitive data (app secret) wherever you choose, like in the Azure Key Vault or other secure storage.
In addition to the user ID and username, custom claims can be generated by writing your own profile service implementation. This example includes a ProfileService which returns user roles from our database as claims (but can use any other source of data). Number and type of claims is not limited and should suit your needs whatever they might be. If we try signing in as alice in the MVC client and decode the generated token, we can see our role claim is indeed included:
Additionally, the profile service implements the IsActive method that checks whether the user account is still active on token generation/refresh. For simplicity, our IsActive method returns true for all users, all the time.
Protecting your keys
Duende Identity Server has the option to manage, rotate, and announce new keys. Keeping private keys safe and confidential is imperative, since leaking a key (or allowing a third-party to create a new valid key) would allow malicious actors to generate valid tokens. By default, keys are stored on disk, but ASP.NET Core Data Protection offers a built-in way to secure these keys. A very elegant way of storing the keys combines cheap and readily available Azure Blob Storage with the Azure Key Vault. Signing keys are encrypted using a key stored in Key Vault and stored in cheap storage. The same process is applied when decrypting the keys. This way, you can leverage RBAC and the Key Vault in order to lock down access to your signing keys.
Azure AD integration
The first step in adding an Azure Active Directory instance as the external provider is registering your application in Azure AD. When registering the app, you will probably want to limit accounts only to your tenant (single tenant). For AAD integration to work the following must be configured:
- callback path URI
- application secret must be configured in Azure AD and Identity Server
- authority URI must be configured on the Identity Server side
It is important to note that, if you want any information on the user (like first/last name, mail, etc.), you need to configure optional claims for the access token that Azure AD returns to Identity Server after successful authentication.
Zooming out: The big picture
When planning the implementation of a solution like Identity Server with the intent of reducing the number of user accounts, we must consider two scenarios; one where our Identity Server becomes the identity provider for other systems (our application, third-party ticketing system, a reporting system otherwise decoupled from our solution etc.), and one where the authentication is offloaded onto external identity providers.
In both cases, upsides are obvious – less user accounts reduces administrative overhead, makes for a better user experience, and reduces the number of attack vectors / prevents bad practices such as password reuse or other human errors (such as writing passwords down and leaving them in plain view).
While this sounds like excellent news, it is paramount to be aware of all the downsides. Since the identity server now becomes a single point of failure for several systems, additional planning and failsafes should be implemented to avoid having your day-to-day operations come to a screeching halt. Imagine the following scenario: your identity server suddenly goes down due to an unforeseen outage. Not only is your application inaccessible, but nobody can report the issue through the usual channels (such as creating a ticket), because, well, they can’t authenticate. Delegating authentication to a third-party provider can also be a double-edged sword. Sure, it is a great tool to allow customer to impose additional restrictions and deny access to unauthorized devices or IP ranges. Control over account provisioning can also be offloaded to the company using our enterprise solution, reducing the load on our user support system. As tempting as reducing your workload can be, bear in mind that the more control you give to the users, the larger the security issue you are potentially exposing yourself to.