KWops - Identity

In this lab, you will learn how to do authentication & authorization with OpenID Connect (OIDC).

Introduction

It’s often necessary for resources and APIs published by a service to be limited to certain trusted users or clients. The first step to making these sorts of API-level trust decisions is authentication. Authentication is the process of reliably verifying a user’s identity. In microservice scenarios, this is typically handled centrally in a microservice dedicated to managing the identity of the users of the system. We will call this the "Identity" service from now on.

When working with RESTful endpoints, authentication is done using bearer tokens issued by the identity service. Client applications retrieve a bearer token from the identity service using the OpenID Connect (OIDC) protocol. When a client calls an api, its sends the bearer token in the Authorization header. The api then verifies the bearer token.

Step 1: Add Identity microservice

We will use the IdentityServer framework from Duende software to create an ASP.NET Core application that can handle login logic and workflow using the OpenID Connect protocol. This application will be the "Identity" microservice.

To better understand what we will do next, it is recommended to read about the "Fundamentals" described in the IdentityServer docs. The next part of the lab is based on the quickstart guide of "IdentityServer".

  • In the file system, create a physical folder "Identity" in the "Services" folder

    • Open a command prompt at this location

    • Install Duende.IdentityServer project templates:

      dotnet new install Duende.IdentityServer.Templates
    • Add a ASP.NET Core Razor Pages project using the "isinmem" template:

      dotnet new isinmem -n Identity.UI
      • This command adds basic IdentityServer functionalities and a basic user interface (most importantly, a login page).

  • Open the KWops solution in Visual Studio

    • Add an Identity solution folder under Services

    • Right click on the Identity folder. Select Add and then Existing project. Add the generated Identity.UI project.

    • Update the Duende.IdentityServer NuGet package to version 7.4.11.

      • Because of this update, you will have to replace all using IdentityModel; statements in the project with using Duende.IdentityModel; By building the project, you will see all the places where this is needed.

      • This update also pulls in a newer Duende.IdentityModel version that removed its own Base64Url helper. In Pages/Diagnostics/ViewModel.cs, replace using Duende.IdentityModel; with using System.Buffers.Text; and change Base64Url.Decode(encoded) to Base64Url.DecodeFromChars(encoded).

    • Update the Serilog.AspNetCore NuGet package (scaffolded by the project template) to version 10.0.0.

    • Now the Identity.UI project should build without errors.

  • Add Container support for the Identity.UI project.

    Identity Server
    • Select the default image Distro and use the solution as the Docker build context.

  • Add Container orchestrator support for the Identity.UI project, like we have done in the first lab.

    • The identity.ui service is automatically added to docker-compose.yml and docker-compose.override.yml, but Visual Studio doesn’t fix the host ports (it leaves them as a single container port, which Docker maps to a random host port each run) or set up the HTTPS certificate. Alter the identity.ui section of the override file to look like this:

      docker-compose.override.yml
        identity.ui:
          environment:
            - ASPNETCORE_ENVIRONMENT=Development
            - ASPNETCORE_HTTP_PORTS=8080
            - ASPNETCORE_HTTPS_PORTS=8081
            - ASPNETCORE_Kestrel__Certificates__Default__Password=Secur3Pass@word
            - ASPNETCORE_Kestrel__Certificates__Default__Path=/home/app/.aspnet/https/aspnetapp.pfx
          ports:
            - "9000:8080"
            - "9001:8081"
      • The ports entries change from a bare container port (e.g. "8080") to a "<host port>:<container port>" mapping, making the service reachable on the host at 9000 (http) and 9001 (https).

      • The two ASPNETCORE_KestrelCertificatesDefault__* variables configure the HTTPS development certificate, the same way you already did for the HR api in the first lab.

  • In the "Identity.UI" project:

    • Disable key management and use developer signing credentials in "HostingExtensions.cs":

      HostingExtensions.cs
      var isBuilder = builder.Services.AddIdentityServer(options =>
          {
              ...
      
              // prevent automatic key management
              // see: https://docs.duendesoftware.com/identityserver/fundamentals/key-management/
              options.KeyManagement.Enabled = false;
          })
          .AddTestUsers(TestUsers.Users);
      
      isBuilder.AddDeveloperSigningCredential(); // not recommended for production - you need to store your keys somewhere secure
      • This way we don’t have to worry about key management and can use the developer signing credentials. This is not recommended for production environments!

    • Comment out the AddGoogle method call in "HostingExtensions.cs" (but keep the AddAuthentication call):

      HostingExtensions.cs
      builder.Services.AddAuthentication();
          //.AddGoogle(options =>
          //{
          //    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
      
          //    // register your IdentityServer with Google at https://console.developers.google.com
          //    // enable the Google+ API
          //    // set the redirect URI to https://localhost:5001/signin-google
          //    options.ClientId = "copy client ID from Google here";
          //    options.ClientSecret = "copy client secret from Google here";
          //});
      • Note that the ConfigureServices method of HostingExtensions is called from Program.cs

    • Comment out the first lines in the OnGet method of the Diagnostics page model in the Pages/Diagnostics folder. That code won’t work when the application is running inside a container.

      Index.cshtml
      ...
      public async Task<IActionResult> OnGet()
      {
          //var localAddresses = new List<string?> { "127.0.0.1", "::1" };
          //if(HttpContext.Connection.LocalIpAddress != null)
          //{
          //    localAddresses.Add(HttpContext.Connection.LocalIpAddress.ToString());
          //}
      
          //if (!localAddresses.Contains(HttpContext.Connection.RemoteIpAddress?.ToString()))
          //{
          //    return NotFound();
          //}
      
          View = new ViewModel(await HttpContext.AuthenticateAsync());
      
          return Page();
      }
      ...
  • Start the application using Docker compose. Navigate to https://localhost:9001

    • Click on the link to see the claims for your current session.

      Identity Server
    • Login with one of the test users (alice, bob):

      Identity Server
      • The test users can be found in the TestUsers class in the Pages folder. You can find the id, username and password of the users. You can also see the claims that are known to be true for those users. If you want to, you can add your own users here. The testusers are added to the system because of the AddTestUsers extension method call in HostingExtensions.cs

        Of course, in a production application, the users would not be hardcoded in a class, but stored in a database. In that case you might want to check out the Entity Framework Core quickstart guide and the Using ASP.NET Core Identity quickstart guide, but this is out of scope for this lab.

    • After the login, you should see the requested claims of the user together with properties of the issued cookie:

      Identity Server

Step 2: Configure Identity microservice

In the next step we will create a console application that can call the HR and DevOps APIs. Those APIs will be secured soon.

First we need to define the resources that need protection. Therefore we will define 3 api scopes:

  • devops.read: read access on the DevOps api

  • hr.read: read access on the HR api

  • manage: write access (in general)

So the ApiScopes property in the Config class of the "Identity.UI" project should look like this:

Config.cs
public static IEnumerable<ApiScope> ApiScopes =>
    new ApiScope[]
    {
        new ApiScope("devops.read", "Read access on DevOps Api"),
        new ApiScope("hr.read", "Read access on HumanResources Api"),
        new ApiScope("manage", "Write access")
    };

When the scopes are defined, the Api resources can be defined. Add a new static property ApiResources:

Config.cs
public static IEnumerable<ApiResource> ApiResources =>
    new List<ApiResource>
    {
        new ApiResource("devops", "DevOps API")
        {
            Scopes = { "devops.read", "manage" },

        },
        new ApiResource("hr", "Human Resources API")
        {
            Scopes = { "hr.read", "manage" }
        }
    };

As you can see, the manage scope is linked to both api resources.

Now you need to add these resources: in the ConfigureServices method of HostingExtensions, add a call to isBuilder.AddInMemoryApiResources:

HostingExtensions.cs
...
// in-memory, code config
isBuilder.AddInMemoryIdentityResources(Config.IdentityResources);
isBuilder.AddInMemoryApiScopes(Config.ApiScopes);
isBuilder.AddInMemoryClients(Config.Clients);
isBuilder.AddInMemoryApiResources(Config.ApiResources); // Add this line!
...

Now we must register the console application as a client in the Identity service. The console client application should use the OIDC protocol with code flow.

The Clients property in the Config class should look like this:

Config.cs
public static IEnumerable<Client> Clients =>
new Client[]
{
        new Client
        {
            ClientId = "kwops.cli",
            ClientName = "KWops Command Line Interface",
            ClientSecrets = { new Secret("SuperSecretClientSecret".Sha256()) },
            AllowedGrantTypes = GrantTypes.Code,
            RedirectUris = { "http://localhost:7890/" },
            AllowOfflineAccess = true,
            AllowedScopes = { "openid", "profile", "devops.read", "hr.read" }
        }
};
  • The client gets an id (kwops.cli) and a secret (SuperSecretClientSecret).

  • By setting the AllowedGrantTypes to code, the client must use the code flow when requesting a token.

  • The RedirectUris property contains the urls to which the user will get send back to after logging in. The authentication code that is needed to retrieve a token, will be sent to the redirect url.

  • By setting AllowOfflineAccess to true, the client can also receive a refresh token (out of scope for this course).

  • The AllowedScopes list, contains the scopes that the client is allowed to request. By excluding the manage scope, the console application will not be able to get write access to our APIs.

Step 3: Secure the DevOps api

KWsoft wants the DevOps api secured in the following way:

  • all endpoints (controller actions) are only accessible for an authenticated user.

  • all endpoints are only accessible for client applications that have access to the devops.read scope.

  • endpoints that allow to change the state of the system (write actions) are only accessible for client applications that have access to the manage scope.

Let’s pour this in code:

  • Add an "Urls" section in "appsettings.json" of the "DevOps.Api" project:

    appsettings.json
    "Urls": {
        "IdentityUrlBackChannel": "https://localhost:9001",
        "IdentityUrlFrontChannel": "https://localhost:9001"
      }
    • These urls point to the "Identity" service. There are 2 different url’s to point to the "Identity" service.

      • Backchannel URL (IdentityUrlBackChannel): this URL is used internally within the system, specifically by services like the "devops.api" container, to communicate directly with the "Identity" service. The backchannel is typically secured, not exposed to external clients, and often used for server-to-server communication within the internal network.

      • Frontchannel URL (IdentityUrlFrontChannel): this URL is intended for external clients, such as browsers or console applications, to access the "Identity" service. The frontchannel is exposed to users outside the system.

      • The url of the "Identity" service from within the "devops.api" container is http://identity.ui (the name of the identity container). So in "docker-compose.override.yml" override the IdentityUrl value by setting an environment variable:

        docker-compose.override.yml
          devops.api:
            environment:
              - ASPNETCORE_ENVIRONMENT=Development
              - ASPNETCORE_HTTP_PORTS=8080
              - ASPNETCORE_HTTPS_PORTS=8081
              - ASPNETCORE_Kestrel__Certificates__Default__Password=Secur3Pass@word
              - ASPNETCORE_Kestrel__Certificates__Default__Path=/home/app/.aspnet/https/aspnetapp.pfx
              - ConnectionStrings__DefaultConnection=Data Source=sqldata;Database=KWops.DevOps;User Id=sa;Password=Pass@word;TrustServerCertificate=True;
              - EventBus__RabbitMQ__Host=rabbitmq
              - Urls__IdentityUrlBackChannel=http://identity.ui
        Important
        Notice how a double underscore (_) is used to set a setting (_IdentityUrlBackChannel) within a section (Urls).
  • To make sure the application looks for bearer tokens in the Authorization header of each incoming request, register and configure the services for authentication in the DI container. Add this anywhere among the other builder.Services.Add…​() calls, before var app = builder.Build(); The exact position doesn’t matter since these registrations don’t depend on each other. You will need to install the nuget packages Microsoft.AspNetCore.Authentication.JwtBearer and Microsoft.IdentityModel.JsonWebTokens (latest stable versions):

    Program.cs
    ...
    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
        {
            string identityBackChannelUrl = builder.Configuration.GetValue<string>("Urls:IdentityUrlBackChannel")!;
            options.Authority = identityBackChannelUrl;
            options.Audience = "devops";
            options.RequireHttpsMetadata = false;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = false,
    
                //HACK: the code below bypasses validating the signature of the JWT token. DO NOT DO THIS IN REAL LIFE APPLICATIONS!
                // This is done because the libraries used to generate and handle tokens must be exactly the same version.
                // The fix proposed in https://docs.duendesoftware.com/identityserver/v7/troubleshooting/wilson/ did not work,
                // so we decided to bypass the signature validation.
                SignatureValidator = (string token, TokenValidationParameters parameters) =>
                {
                    var jwt = new JsonWebToken(token);
    
                    return jwt;
                }
            };
            options.Events = new JwtBearerEvents
            {
                OnAuthenticationFailed = context =>
                {
                    var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
                    logger.LogWarning(context.Exception, "Bearer token authentication failed");
                    return System.Threading.Tasks.Task.CompletedTask;
                },
                OnForbidden = context =>
                {
                    var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
                    Exception? failure = context.Result?.Failure;
                    if (failure is not null)
                    {
                        logger.LogWarning(failure, "Bearer token authorization failed");
                    }
                    else
                    {
                        logger.LogWarning("Tried to access forbidden resource");
                    }
                    return System.Threading.Tasks.Task.CompletedTask;
                }
            };
        });
    
    var app = builder.Build();
    ...
    • Let’s breakdown what is happening in this code:

      • builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme): this sets bearer (JwtBearerDefaults.AuthenticationScheme) as the default scheme. This will make the application look for a bearer token in the Authorization header by default (instead of looking for a cookie, for example).

      • options.Authority = identityBackChannelUrl: this (backchannel) url is used to validate the signature of the bearer token. Via the url [identityBackChannelUrl]/.well-known/openid-configuration the public key is retrieved from the "Identity" service which can then be used to verify the signature.

        Tip
        Try to surf to https://localhost:9001/.well-known/openid-configuration yourself to see the information that the "Identity" service is exposing. Of course the "Identity" service needs to be running for this.
      • options.Audience = "devops": the "Identity" service will add the name of the api resource that was requested in the token as an audience the token is meant for. By setting the Audience property, the api checks if the token that is received is meant for the devops audience.

      • options.RequireHttpsMetadata = false: the communication that happens when the devops.api container retrieves the signing certificate from the identity.ui container, cannot happen via https because we did not configure the ssl certificates. So we do not require that this communication must go via https.

      • Setting the TokenValidationParameters: the application must not validate the issuer of the token. This is because the "Identity" service sets the issuer to https://localhost:9001 which does not match the Authority url we use (http://identity.ui). Also, the signature of the token is not validated. This is because the libraries used to generate and handle tokens must be exactly the same version. The fix proposed in https://docs.duendesoftware.com/identityserver/v7/troubleshooting/wilson/ did not work, so we pragmatically decided to bypass the signature validation.

      • options.Events: here we define what should happen when the authentication fails or when the user is not allowed to access a resource. In this case we log a warning message.

  • Add a filter that enforces all controller actions to have an authenticated user that has a "scope" claim with the value "devops.read". Add the readPolicy variable right after the last builder.Services.Add…​() call, and add the AuthorizeFilter line to the existing AddControllers call from the second lab (the rest of the block below is shown for context — it’s already there):

    Program.cs
    var readPolicy = new AuthorizationPolicyBuilder()
        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
        .RequireAuthenticatedUser()
        .RequireClaim("scope", "devops.read")
        .Build();
    
    builder.Services.AddSingleton(provider => new ApplicationExceptionFilterAttribute(provider.GetRequiredService<ILogger<ApplicationExceptionFilterAttribute>>()));
    builder.Services.AddControllers(options =>
    {
        options.Filters.AddService<ApplicationExceptionFilterAttribute>();
        options.Filters.Add(new AuthorizeFilter(readPolicy)); // Add this line!
    });
    ...
    • First a policy is created. The policy states that it is a policy for bearer tokens that requires the client to be authenticated and have a claim scope in the token with value devops.read.

    • Then the policy is used to add an AuthorizeFilter next to the ApplicationExceptionFilter you already added in the second lab.

  • Define a (second) policy that can be used to secure controller actions that need write access. Add that policy to the DI container, right after the AddAuthentication()/AddJwtBearer() call you just added, and before var app = builder.Build();:

    Program.cs
    ...
    var writePolicy = new AuthorizationPolicyBuilder()
        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
        .RequireAuthenticatedUser()
        .RequireClaim("scope", "manage")
        .Build();
    
    builder.Services.AddAuthorization(options =>
    {
        options.AddPolicy("write", writePolicy);
    });
    ...
    • The policy requires a user to be authenticated and have a scope claim with the value manage.

    • The policy is registered in the DI container with the name write.

  • Secure the AssembleTeam action method of the TeamsController. Enforce the write policy for this action method:

    TeamsController.cs
    [ApiController]
    [Route("api/[controller]")]
    public class TeamsController : ControllerBase
    {
        ...
    
        [HttpPost("{id}/assemble")]
        [Authorize(policy:"write")]
        public async Task<IActionResult> AssembleTeam(Guid id, TeamAssembleInputModel model)
        {
            ...
        }
    }

You will now get 401 (Unauthorized) responses from the api when you use the Swagger client application.

Step 4: Secure the HR api

Repeat the actions of the previous step for the Human Resources api. Also define a read policy and a write policy. Enforce the read policy on all action methods and the write policy on the Add and Dismiss actions of the EmployeesController.

Step 5: Create a native client

Now we will create the client application that was configured in the "Identity" service. The client application is a (dummy) console application. It is a command-line interface to communicate with the devops and hr APIs.

  • Download the KWops.Cli project from blackboard.

  • In the file system, add a Clients folder next to the Services and SharedKernel folders.

  • In the file system, copy the KWops.Cli folder in the Clients folder.

  • In VS, add a solution folder Clients.

  • In VS, right click on the Clients solution folder. Choose "Add" → "Existing Project…​". Navigate to the "csproj" file of the KWops.Cli project and select it.

Without going into too much detail, we will explain the concepts in the project you just added:

  • Program.cs

    • When a key is pressed, the OIDC protocol is used to retrieve an access token from the "Identity" service. The OidcService class contains the necessary logic to achieve this.

    • When the access token is retrieved, it is used to get some data from the devops api. The DevOpsApiClient class contains the necessary logic to achieve this.

    • When a key is pressed, the application closes.

  • OidcService.cs

    • Makes use of the Duende.IdentityModel OIDC Client. This is a NuGet package that contains an OidcClient class that implements the OAuth2 / OIDC protocol. It can be used in all sorts of .NET native applications like a console application or mobile application (Maui).

    • Uses the manual mode to open a browser window to redirect the user to the "Identity" service. After the user has proven its identity in with the "Identity" service, the "Identity" service sends (redirects) the user to "http://localhost:7890" with an authentication code.

      • When the user is redirected to the "Identity" service, the following information is delivered to the "Identity" service via query parameters in the url (OidcClientOptions):

        • Authority → url of the "Identity" service

        • ClientId, ClientSecret → the id and secret of the client as registered in the "Identity" service.

        • Scope → the scopes that we request. The openid (mandatory) and profile scopes will make sure certain claims can be found in the identity token (sub (user identifier), name, family_name, given_name, website, …​)

        • RedirectUrl → The url to which the user is send back to after it has proven itself on the "Identity" website.

Let’s try it:

  • Start the microservices (docker-compose).

  • Right click on the "KWops.Cli" project. Select "Debug" and then "Start new instance".

    Start debugging client
  • Press any key. A browser window will open.

    Start OIDC protocol
    • Take a look at the querystring of the url. It contains all the information that is passed from the client application (cli) to the "Identity" service.

  • Login. You are now redirected to http://localhost:7890. The query string contains the code that the client (cli) can use to request an identity token and access token. Normally the console applications should be listening for requests on this address and automatically process it. In this lab we will do it manually.

    Response of identity server after login
  • Copy the whole querystring (everything after the question mark) and paste it in the console window. Press enter.

    Process response
  • An identity token and an access token is retrieved from the "Identity" service (ProcessResponseAsync method). The claims in the identity token are printed in the console and the access token is printed in the console.

    Claims in identity token and access token
  • Then the access token is used to retrieve all teams from the DevOps api. The results are printed to the console window.

    Teams

Step 6: Adjust swagger clients

There are actually 3 client applications in the solution now:

  • The KWops.Cli

  • The swagger ui application for the hr api (which is a client application that runs in a browser and makes calls to the api from within the browser)

  • The swagger ui application for the devops api

To be able to still use the swagger ui’s, some login functionality needs to be added so that a bearer token can be sent to the api when an endpoint is executed in the ui.

  • Register two more clients in the "Identity" service:

    • Add two more clients

      Config.cs
      public static IEnumerable<Client> Clients =>
      new Client[]
      {
              new Client
              {
                  ClientId = "kwops.cli",
                  ClientName = "KWops Command Line Interface",
                  ClientSecrets = { new Secret("SuperSecretClientSecret".Sha256()) },
                  AllowedGrantTypes = GrantTypes.Code,
                  RedirectUris = { "http://localhost:7890/" },
                  AllowOfflineAccess = true,
                  AllowedScopes = { "openid", "profile", "devops.read", "hr.read" }
              },
              new Client
              {
                  ClientId = "swagger.devops",
                  ClientName = "Swagger UI for DevOps Api",
                  AllowedGrantTypes = GrantTypes.Code,
                  RequireClientSecret = false,
                  RedirectUris = {"https://localhost:8001/swagger/oauth2-redirect.html"},
                  AllowedCorsOrigins = {"https://localhost:8001"},
                  AllowedScopes = {"devops.read", "manage"}
              }
              ,
              new Client
              {
                  ClientId = "swagger.hr",
                  ClientName = "Swagger UI for Human Resources Api",
                  AllowedGrantTypes = GrantTypes.Code,
                  RequireClientSecret = false,
                  RedirectUris = {"https://localhost:5001/swagger/oauth2-redirect.html"},
                  AllowedCorsOrigins = {"https://localhost:5001"},
                  AllowedScopes = {"hr.read", "manage"}
              }
      };
      • Notice that the 2 swagger clients don’t have a client secret. When the redirect to the "Identity" service is initiated from a browser window (which will be the case for our swagger ui’s), it is not possible to get the client secret to that browser window in a secure way. For the "Identity" service to accept requests from the swagger clients, the RequireClientSecret property is set to false.

      • Notice the redirect uri that is set.

      • The AllowedCorsOrigins property, makes sure that the "Identity" service will only accept requests coming from that url when authenticating a client.

      • Notice the scopes that are allowed for each client.

  • Add the Swashbuckle.AspNetCore NuGet package to the Api project in the SharedKernel folder.

  • Add a folder Swagger in the Api project.

    • Add a class AlwaysAuthorizeOperationFilter in the Swagger folder:

      AlwaysAuthorizeOperationFilter.cs
      using Microsoft.OpenApi;
      using Swashbuckle.AspNetCore.SwaggerGen;
      
      namespace Api.Swagger
      {
          public class AlwaysAuthorizeOperationFilter : IOperationFilter
          {
              private readonly string _securityScheme;
              private readonly string[] _scopes;
      
              public AlwaysAuthorizeOperationFilter(string securityScheme, string[] scopes)
              {
                  _securityScheme = securityScheme;
                  _scopes = scopes;
              }
      
              public void Apply(OpenApiOperation operation, OperationFilterContext context)
              {
                  operation.Responses ??= new OpenApiResponses();
                  operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
                  operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
      
                  operation.Security = new List<OpenApiSecurityRequirement>
                  {
                      new OpenApiSecurityRequirement
                      {
                          [new OpenApiSecuritySchemeReference(_securityScheme)] = _scopes.ToList()
                      }
                  };
              }
          }
      }
      • This class will be used to enable authorization for all api endpoints in the swagger ui.

  • In the DevOps.Api project:

    • Add OAuth2 support by changing the options in the AddSwaggerGen method in Program.cs:

      Program.cs
      ...
      string identityUrl = builder.Configuration.GetValue<string>("Urls:IdentityUrlFrontChannel")!;
      builder.Services.AddSwaggerGen(c =>
      {
          c.SwaggerDoc("v1", new OpenApiInfo { Title = "DevOps.Api", Version = "v1" });
          string securityScheme = "OpenID";
          var scopes = new Dictionary<string, string>
          {
              { "devops.read", "DevOps API - Read access" },
              { "manage", "Write access" }
          };
          c.AddSecurityDefinition(securityScheme, new OpenApiSecurityScheme
          {
              Type = SecuritySchemeType.OAuth2,
              Flows = new OpenApiOAuthFlows
              {
                  AuthorizationCode = new OpenApiOAuthFlow
                  {
                      AuthorizationUrl = new Uri($"{identityUrl}/connect/authorize"),
                      TokenUrl = new Uri($"{identityUrl}/connect/token"),
                      Scopes = scopes
                  }
              }
          });
          c.OperationFilter<AlwaysAuthorizeOperationFilter>(securityScheme, scopes.Keys.ToArray());
      });
      ...
      • Notice how the available scopes are set.

      • Notice that the frontchannel url of the "Identity" service is used to set the AuthorizationUrl and TokenUrl. This is because the swagger ui will be running in a browser window.

      • Notice how the AuthorizationUrl (url used to request a code) and the TokenUrl (url used to retrieve tokens using a code) are set. These urls can be found at https://localhost:9001/.well-known/openid-configuration (returns info about the openId configuration of the "Identity" service).

    • Add login ui components to the swagger ui by changing the options in the UseSwaggerUI method in "Program.cs":

      Program.cs
      ...
      if (app.Environment.IsDevelopment())
      {
          app.UseSwagger();
          app.UseSwaggerUI(c =>
          {
              c.SwaggerEndpoint("/swagger/v1/swagger.json", "DevOps.Api v1");
              c.OAuthClientId("swagger.devops");
              c.OAuthUsePkce();
          });
      }
      ...
      • Here we set the clientId that will be sent to the "Identity" service.

      • We also say that the more secure "Pkce" method should be used (adds an extra security check when retrieving tokens).

Do the same steps for the HumanResources.Api service. Pay attention to which scopes you define!

Now you can start the application and use the swagger ui’s to authenticate and communicate with the APIs.

The swagger ui now contains an "Authorize" button:

Swagger UI - Authorize button

Click on the button and select the scopes you want to request (may only read access is sufficient?):

Swagger UI - Select scopes to request

Prove who you are with the "Identity" service. After that the "Identity" service will send you back to the application (redirect url set in "Identity" service). The swagger ui processes the response and retrieves an access token.

Swagger UI - Authorization response processed

Click on the "Close" button. Now, the swagger ui will put the access token in the "Authorization" header of each request.