Using Identity Server 4 in a Distributed Monolith

Some time ago we faced the task of differentiating access rights to resources, i.e. the task of authentication and authentication management. Since the architecture of the main projects was something similar to a distributed monolith, we decided to stop at Identity Server.

Identity Server

Probably the best way to understand what Identity Server is is to read documentation. In short, I can say that IS is OpenID Connect and OAuth 2.0 framework for ASP.NET Core. It is worth noting that the potential of the framework is quite huge.

If we talk about the methods of its application, then, in general, two main ones can be noted:

  • inside the application – this is a way to embed the framework into the main application or system core. This method is good for the speed of operation, as well as the ability to combine logic and authentication, but is not very good in terms of flexibility and leads to a clutter of code;

  • a separate service is the allocation of a framework into a separate workflow, such as an authentication service, requests to which will go over the network. The advantages here include flexibility and code separation, but you have to pay for it with speed;

Resources that are protected by Identity Server can be file storage, various services providing data, configuration adapters, etc. To restrict access to resources, it is necessary to configure IS. The configuration itself can be stored in various places, for example, the configuration can be stored in a database, interaction with which can be carried out through EF Core.

var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
const string connectionString = …
services.AddIdentityServer()
    .AddConfigurationStore(options =>
    {
        options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
            sql => sql.MigrationsAssembly(migrationsAssembly));
    })
    .AddOperationalStore(options =>
    {
        options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
            sql => sql.MigrationsAssembly(migrationsAssembly));
    });

Here IS is connected via AddIdentityServer()and methods AddConfigurationStore And AddOperationalStore initialize the configuration and load operational data from the database. If you use a database, you can change the Identity Server configuration on the fly without having to update the service.

The configuration can also be stored in a file, in which case, when the application is launched, it will be loaded into RAM.

services.AddIdentityServer()
        .AddInMemoryClients(Clients)
        .AddInMemoryIdentityResources(Resources)
        .AddInMemoryApiResources(ApiResources)
        .AddInMemoryApiScopes(Scopes);

Loading here Clients, Resources, Scopes, ApiResources, ApiScopes into RAM, which will be discussed below.

Configuration Features

From previous code inserts, you might have noticed a configuration set in the form of Clients, Resources, ApiResources, Scopes. These are probably the main configurations that need to be built into the project, but what do they mean? Let's figure it out

Clients

Clients are clients that can connect to the IS and get permission to use some resources or areas.

{
        "ClientId": "client_id",
        "ClientSecrets": [ { "Value": "xxx" } ],
        "AccessTokenLifetime": "86400",
        "AllowedGrantTypes": [ "client_credentials" ],
        "AllowedScopes": [
          "openid",
          "profile",
        ]
}

Like every user in a modern application, the client configuration has a login and password. The login here is “ClientId”, and the password is “ClientSecrets” in encrypted form.

It is important to understand that clients are usually applications – this leads us to the need to have different types of client authentication, this is the responsibility of the parameter “AllowedGrantTypes” in the client configuration. Let's consider the main 5 non-hybrid types, each of them is designed for its own login scenario.

  • client credentials – intended for machine-to-machine communication – the token is requested directly on behalf of the client;

  • authorization code — designed to work with interactive users of the client application;

  • device flow – designed to work with devices without a browser or with limited input capabilities, such as Apple TV;

  • implicit – is becoming increasingly obsolete these days, as it was intended for native and JS applications where the access token was returned immediately without the extra step of exchanging an authorization code;

  • resource Owner Password – used in case of trusted relationships with the client, for example for applications with a high level of privileges;

The access received would be infinite if there were no time limits in the depths of the Identity Server, which are configured by the “AccessTokenLifetime“. AccessTokenLifetimethis is the lifetime of the access token. In addition to the access token, some types of authorization use “RefreshToken“. RefreshToken refresh token, which allows you to extend access, for this you need to set the parameter “AllowOfflineAccess to true and execute a query like this:

POST /connect/token
    client_id=client&
    client_secret=secret&
    grant_type=refresh_token&
    refresh_token=hdh922

Resources

Resources in Identity Server 4 are divided into two types:

  • Identity Resourcesthese are user resources such as: user ID, login, e-mail, and so on;

  • API Resourcesthese are functional resources that a client can access, this may include both API methods and message queues, and other functionality;

For example, a resource can be a file storage or, for example, spaces (scopes are used to separate restrictions, but more on that later)directories inside the storage. Also, a common case is when a resource is a whole service, and spaces are functional parts of the service.

Spaces

Probably the easiest way to understand what spaces are is to imagine a minimal resource, for example, a service that works with users and implements CRUD operations (create, read, update, delete) – these types of operations with the service are spaces, but it is worth noting that there are other options for adapting resources and spaces to the task. For example, spaces can specify certain resources to which access is requested. In particular, we can request access not to all users, but to a certain group that will be specified in the space.

Profile service

The profile service is designed to expand the capabilities for access to user identification data. With its help, you can identify, validate users, as well as influence access, limit it if necessary, add your claims to the token and much more (documentation). It is worth noting that users can be either real users of the system or just parts or modules of the application. The service must implement the IProfileService interface and add to the pipeline:

services.AddIdentityServer()    
        .AddProfileService<ProfileService>();

Validation service

In addition to ProfileService, you can implement your own validation service that will check both the access token and the entire request. Validation is performed after the request is executed through the main authentication logic and before sending a response to the client. With its help, you can change the access token, influence the information in this token, or restrict access to certain entities (more details). The service must implement the ICustomTokenRequestValidator interface and embed it into the pipeline:

services.AddIdentityServer()    
        .AddCustomTokenRequestValidator<ClientTokenValidatorService>();

Application

So, we have configured the Identity Server and also decided to keep it separate as an independent service that issues access tokens to certain resources with the correct request parameters. Now we need to make services or resources check these tokens. And here too there are two ways:

Checking the token on the resource itself

Checking the token on the resource itself

The first and most obvious is to check the access token on the resource or service itself, this method is the most flexible and fastest. In the figure, each resource has a layer that checks the token and manages access to the resource. In such an implementation, individual settings can be applied to each resource. For example, token validation for file storage can be configured as follows:

var tokenValidationParameters = new TokenValidationParameters
{
   ValidateAudience = false,
   ValidateIssuerSigningKey = true,
   ValidateLifetime = false,
   IssuerSigningKey = securityKey,
   ValidateIssuer = false,
   ClockSkew = TimeSpan.FromMinutes(5)
};
services.AddAuthentication("Bearer")
   .AddJwtBearer("Bearer", options =>
   {
       options.TokenValidationParameters = tokenValidationParameters;
   });

here the Bearer authentication scheme is used, and only the digital signature is validated and an adjustment for the token's operating time is taken into account (relevant if the resource and the IS are on different machines, where the time differs). In turn, some open service for working with users can validate the token according to its own internal rules.

The downside here is that each service must be able to read the token, and therefore have read access keys, and if they need to be changed, all resources that depend on the keys will have to be republished.

Separate token verification service

Separate token verification service

Here a separate service appears, token verification and authentication management. There are also options when the token verification service is built in with Identity Server.

The configuration of validation and reading of the access token will be stored in one place. This approach may be a better alternative, since it solves all the disadvantages of the previous one, but here there is interaction over the network, load balancing and other features of the microservice approach, which generally adds token verification time and, by the way, each resource must know who to contact, which leads us to a single configuration repository, however, this is another story.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *