Tutorial 6: Using Amazon Cognito for apps

This tutorial configures the Amazon Cognito user pool as an SAML identity provider for the installable cloud app deployed via PowerServer.


Task 1: Create the Cognito Web API

Purpose

This task achieves the following results:

  • Creates the Amazon Cognito user pool.

  • Creates a Web API to authenticate the user credential with the Cognito service and return an identity token or an error.

Create the Amazon Cognito user pool

For more details, refer to Getting Started with User Pools.

Step 1: Set up the AWS AWS Single Sign-On (SSO).

Before you can set up AWS Single Sign-On (SSO), you must:

  • Have first set up the AWS Organizations service and have All features set to enabled. For more information about this setting, see Enabling All Features in Your Organization in the AWS Organizations User Guide.

  • Sign in with the AWS Organizations management account credentials before you begin setting up AWS SSO. These credentials are required to enable AWS SSO. For more information, see Creating and Managing an AWS Organization in the AWS Organizations User Guide. You cannot set up AWS SSO while signed in with credentials from an Organization’s member account.

For more details, refer to AWS SSO prerequisites.

Step 2: Get the SAML 2.0 metadata.

1) Add a new application.

2) Add a custom SAML 2.0 application.

3) After filling in the configuration, save it, and then download the SAML metadata file or save the metadata file URL.

For more details, refer to AWS Single Sign-On.

Step 3: Add an identity provider.

1) Click Add provider.

2) Select SAML and then upload the SAML metadata file you just got.

Step 4: Create the user pool.

  1. Go to the Amazon Cognito console. You might be prompted for your AWS credentials.

  2. Choose Manage User Pools.

  3. In the top-right corner of the page, choose Create a user pool.

  4. Provide a name for your user pool, and choose Review defaults to save the name.

  5. In the top-left corner of the page, choose Attributes, choose Email address or phone number and Allow email addresses, and then choose Next step to save.

  6. In the left navigation menu, choose Review.

  7. Review the user pool information and make any necessary changes. When the information is correct, choose Create pool.

Fill in the following configuration as required.

Tips: It is recommended to modify the configuration (for example "Attributes") that cannot be modified after pool creation.

Step 5: Create the user pool application client.

  1. On the navigation bar on the left-side of the page, choose App clients under General settings.

  2. Choose Add an app client.

  3. Give your app a name.

  4. Check Generate client key.

  5. Check Enable authentication based on username and password (ALLOW_USER_PASSWORD_AUTH).

  6. Choose Create an application client.

Step 6: Configure the SAML identity provider.

Open the identity provider configuration page of the user pool, choose SAML, select the SAML metadata file downloaded in step 2 or the terminal node URL of the metadata file.

Step 7: Configure the application integration settings.

  1. Configure domain name. You can configure the Amazon Cognito domain name or your own domain name.

  2. Configure the application client settings, select all options under the Enable identity provider, enter the callback URL and the logout URL, select Authorization code grant and implicit grant under the Allowed OAuth flow, select all options under the Allowed OAuth scope, save the settings and click to publish Hosted UI.

Step 8: Import or create users.

Step 9: Create a group (optional).

Create a Cognito Web API

Step 1: Create an ASP.NET Core Web API project in SnapDevelop and name it AWSServer.

Step 2: Install Amazon.AspNetCore.Identity.Cognito and Amazon.Extensions.CognitoAuthentication to the project through NuGet Package Manager.


Step 3: Open the appsettings.json file and add the AWS Cognito user pool.

"AWS": {
    "Region": "us-west-2",
    "UserPoolId": "us-west-2_5wyOzYn1d",
    "UserPoolClientId": "4linbauf6d58b552r6lc3gbpkc",
    "UserPoolClientSecret": "1prlm08gm3aptlokcbai88ekiegff9mqbc98nhebfart5g4a3cr2"
  }


Step 4: Add a folder named Cognito and add the following files to the folder.

1) Add a class files named LoginUser.cs that includes the following scripts:

namespace AWSServer.Cognito
{
    public class LoginUser
    {
        public string UserName { get; set; }
        public string Password { get; set; }
        public string ClientSecret { get; set; }
    }
}

2) Add a class files named ICognitoUserStore.cs that includes the following scripts:

using System;
using System.Threading;
using System.Threading.Tasks;
using Amazon.CognitoIdentityProvider;
using Amazon.Extensions.CognitoAuthentication;

namespace AWSServer.Cognito
{
    public interface ICognitoUserStore : IDisposable
    {
        IAmazonCognitoIdentityProvider IdP { get; }
        CognitoUserPool UserPool { get; }
        
        Task<AuthFlowResponse> StartValidatePasswordAsync(CognitoUser user, string password, CancellationToken cancellationToken);
    }
}

3) Add a class files named CognitoUserStore.cs that includes the following scripts:

using System;
using System.Threading;
using System.Threading.Tasks;
using Amazon.CognitoIdentityProvider;
using Amazon.CognitoIdentityProvider.Model;
using Amazon.Extensions.CognitoAuthentication;

namespace AWSServer.Cognito
{
    public partial class CognitoUserStore : ICognitoUserStore
    {
        private IAmazonCognitoIdentityProvider _cognitoIdP;
        private CognitoUserPool _cognitoUserPool;
        
        public IAmazonCognitoIdentityProvider IdP
        {
            get => _cognitoIdP;
            private set => _cognitoIdP = value;
        }
        public CognitoUserPool UserPool
        {
            get => _cognitoUserPool;
            private set => _cognitoUserPool = value;
        }
        
        public CognitoUserStore(IAmazonCognitoIdentityProvider cognitoIdPClient, CognitoUserPool userPool)
        {
            IdP = cognitoIdPClient ?? throw new ArgumentNullException(nameof(cognitoIdPClient));
            UserPool = userPool ?? throw new ArgumentNullException(nameof(userPool));
        }
        
        public virtual async Task<AuthFlowResponse> StartValidatePasswordAsync(CognitoUser user, string password, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            try
            {
                AuthFlowResponse context =
                    await user.StartWithSrpAuthAsync(new InitiateSrpAuthRequest()
                    {
                        Password = password
                    }).ConfigureAwait(false);
                    
                return context;
            }
            catch (NotAuthorizedException)
            {
                return null;
            }
        }
        
        #region IDisposable
        private bool disposed = false;
        
        protected virtual void Dispose(bool disposing)
        {
            if (disposed)
                return;
                
            disposed = true;
        }
        
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion
    }
}

4) Add a class files named ICognitoUserValidator.cs that includes the following scripts:

using System.Threading.Tasks;
using Amazon.Extensions.CognitoAuthentication;

namespace AWSServer.Cognito
{
    public interface ICognitoUserValidator
    {
        Task<CognitoUser> ValidateAsync(string userName, string password, string clientSecret);
    }
}

5) Add a class files named CognitoUserValidator.cs that includes the following scripts:

using System.Threading;
using System.Threading.Tasks;
using Amazon.CognitoIdentityProvider;
using Amazon.Extensions.CognitoAuthentication;
using Microsoft.Extensions.Logging;

namespace AWSServer.Cognito
{
    public class CognitoUserValidator : ICognitoUserValidator
    {
        private readonly ICognitoUserStore _userStore;
        private readonly ILogger<CognitoUserValidator> _logger;
        
        public CognitoUserValidator(
            IAmazonCognitoIdentityProvider provider,
            CognitoUserPool userPool,
            ILogger<CognitoUserValidator> logger)
        {
            _userStore = new CognitoUserStore(provider, userPool);
            _logger = logger;
        }
        
        public async Task<CognitoUser> ValidateAsync(string userName, string password, string clientSecret)
        {
            var user = await InternaValidateAsync(userName, password, clientSecret);
            
            if (user.SessionTokens != null && user.SessionTokens.IsValid())
            {
                return user;
            }
            else
            {
                return null;
            }
        }
        
        internal async Task<CognitoUser> InternaValidateAsync(string username, string password, string clientSecret)
        {
            var user = new CognitoUser(username, _userStore.UserPool.ClientID, _userStore.UserPool, _userStore.IdP, clientSecret: clientSecret);
            
            var authResponse = await _userStore.StartValidatePasswordAsync(user, password, CancellationToken.None);
            
            if (authResponse != null)
            {
                _logger.LogInformation($"\nUser <{username}> logged in.");
                return user;
            }
            else
            {
                _logger.LogInformation("\nInvalid login attempt.");
                return null;
            }
        }
        
    }
}

6) Add a class files named AmazonCognitoIdPClientFactory.cs that includes the following scripts:

using Amazon;
using Amazon.CognitoIdentityProvider;
using Amazon.Runtime;

namespace AWSServer.Cognito
{
    public sealed class AmazonCognitoIdPClientFactory
    {
        public static AmazonCognitoIdentityProviderClient IdPClient { get; private set; }
        
        public static AmazonCognitoIdentityProviderClient CreateIdPClient(RegionEndpoint regionEndpoint)
        {
            IdPClient = new AmazonCognitoIdentityProviderClient(new AnonymousAWSCredentials(), regionEndpoint);
            
            return IdPClient;
        }
    }
}

7) Add a class files named CognitoGroupAuthorizationRequirement.cs that includes the following scripts:

using Microsoft.AspNetCore.Authorization;

namespace AWSServer.Cognito
{
    public class CognitoGroupAuthorizationRequirement : IAuthorizationRequirement
    {
        public string CognitoGroup { get; private set; }
        
        public CognitoGroupAuthorizationRequirement(string cognitoGroup)
        {
            CognitoGroup = cognitoGroup;
        }
    }
}

8) Add a class files named CognitoGroupAuthorizationHandler.cs that includes the following scripts:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;

namespace AWSServer.Cognito
{
    public class CognitoGroupAuthorizationHandler : AuthorizationHandler<CognitoGroupAuthorizationRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CognitoGroupAuthorizationRequirement requirement)
        {
            if (context.User.HasClaim(c => c.Type == "cognito:groups" &&
                                           requirement.CognitoGroup.Contains(c.Value)))
            {
                context.Succeed(requirement);
            }
            else
            {
                context.Fail();
            }
            
            return Task.CompletedTask;
        }
    }
}

9) Add a class files named CognitoInjectorServiceExtensions.cs that includes the following scripts:

using Amazon.CognitoIdentityProvider;
using Amazon.Extensions.CognitoAuthentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace AWSServer.Cognito
{
    public static class CognitoInjectorServiceExtensions
    {
        public static IServiceCollection AddCognitoServices(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddSingleton<IAuthorizationHandler, CognitoGroupAuthorizationHandler>();
            services.AddSingleton<IAmazonCognitoIdentityProvider>(
                            AmazonCognitoIdPClientFactory.CreateIdPClient(configuration.GetAWSOptions().Region));
            services.AddSingleton(new CognitoUserPool(
                            configuration.GetAWSCognitoClientOptions().UserPoolId,
                            configuration.GetAWSCognitoClientOptions().UserPoolClientId,
                            AmazonCognitoIdPClientFactory.IdPClient));
            services.AddScoped<ICognitoUserValidator, CognitoUserValidator>();
            
            return services;
        }
        
    }
}

Step 6: Create a class file named CognitoController.cs which is derived from controller.

using System.Threading.Tasks;
using AWSServer.Cognito;
using Microsoft.AspNetCore.Mvc;

namespace AWSServer.Controllers
{
    [Route("[controller]/[action]")]
    public class CognitoController : ControllerBase
    {
        private readonly ICognitoUserValidator _cognitoUserValidator;
        
        public CognitoController(ICognitoUserValidator validator)
        {
            _cognitoUserValidator = validator;
        }
        
        [HttpPost]
        public async Task<ActionResult> GetToken([FromBody] LoginUser lgUser)
        {
            await Task.CompletedTask;
            var user = _cognitoUserValidator.ValidateAsync(lgUser.UserName, lgUser.Password, lgUser.ClientSecret).Result;
            
            return Ok(new
            {
                access_token = user.SessionTokens.AccessToken,
                token_type = "Bearer",
                expires_in = "3600"
            });
            
        }
    }
}

Step 7: Modify the Startup.cs class.

1) Add the following namespace:

using AWSServer.Cognito;

2) Add the Cognito service in the ConfigureServices method.

services.AddCognitoServices(Configuration);


Step 8: Save the changes and click Run from the SnapDevelop toolbar to start the Web API.

Step 9: Test the Web API by sending a request.

  1. Right click in the code block of a method, and select Run Test(s) from the popup menu.

    The Web API Tester is launched.

  2. In the Web API Tester, click the plus (+) sign to create a new request:

    URL: https://localhost:5001/Cognito/getToken or http://localhost:5000/Cognito/getToken (you can modify the IP address and port number in the launchSettings.json file)

    HTTP method: POST

    Content-Type: application/json

    Request:

    {"username":"admin@test.com", "password":"appeon123", "ClientSecret":"1prlm08gm3aptlokcbai88ekiegff9mqbc98nhebfart5g4a3cr2"}
    
  3. Click Send to send the request, and the API returns the token information if validation is successful.


Task 2: Modify the PowerBuilder client app

Purpose

This task modifies the PowerBuilder application source code and the PowerServer project settings to achieve the following results:

  • Gets the user credential from the application login window, then authenticates it with the Amazon Cognito User Pools and gets an identity token.

  • Uses the identity token to access data from the PowerServer Web API.

  • Refreshes the identity token when necessary.

Preparations

Before you make changes to the PowerBuilder client app, make sure the application can run successfully. In this tutorial, we will take Sales Demo as an example.

Step 1: Select Windows Start | Appeon PowerBuilder 2021, and then right-click Example Sales App and select More | Run as administrator.

Step 2: When the SalesDemo workspace is loaded in the PowerBuilder IDE, click the Run button in the PowerBuilder toolbar.

Step 3: When the application main window is opened, click the Address icon in the application ribbon bar and make sure data can be successfully retrieved.

Step 4: After that, follow instructions in the Quick Start guide to configure and deploy the application as an installable cloud app using the PowerServer deployment option. Make sure the installable cloud app can run successfully.

Add scripts

Step 1: Declare the following global variables.

//Token expiresin
Long gl_Expiresin
//Refresh token clockskew 
Long gl_ClockSkew = 3

Step 2: Define a global function and name it f_Authorization().

Select from menu File > New; in the New dialog, select the PB Object tab and then select Function and click OK to add a global function.

This global function uses the HTTP Post method to send the user credentials to the authorization server and then gets the identity token from the HTTP Authorization header.

Add scripts to the f_Authorization() function to implement the following scenario: When the application starts, the application uses the username and password from the login window to get the token, and when the token expires, the login window displays for the user to input the username and password again.

Note: The following scripts will work only after you implement a login window and return the username and password to the f_Authorization() function.

//f_Authorization() for password
//UserName & Password from login window
RestClient lrc_Client
String     ls_url, ls_UserName, ls_UserPass, ls_PostData, ls_Response, ls_expires_in, ls_ClientSecret
String     ls_TokenType, ls_AccessToken
String     ls_type, ls_description, ls_uri, ls_state
Integer    li_Return, li_rtn
JsonParser ljson_Parser

li_rtn = -1
ls_url = profilestring("CloudSetting.ini","setup","OauthURL","")

//login window can be implemented as actual needs
//Open(w_login) 
//Return: UserName & Password

ls_UserName = "admin@test.com"
ls_UserPass = "appeon123"
ls_ClientSecret = "1prlm08gm3aptlokcbai88ekiegff9mqbc98nhebfart5g4a3cr2"

If IsNull ( ls_UserName ) Or Len ( ls_UserName ) = 0 Then
 MessageBox( "Tips", "UserName is empty!" )
 Return li_rtn
End If
If IsNull ( ls_UserPass ) Or Len ( ls_UserPass ) = 0 Then
 MessageBox( "Tips", "Password is empty!" )
 Return li_rtn
End If

ls_PostData = '{"username":"' + ls_UserName + '", "password":"' + ls_UserPass + '", "clientsecret":"' + ls_ClientSecret + '"}';
lrc_Client = Create RestClient
lrc_Client.SetRequestHeader("Content-Type","application/json")
li_Return = lrc_Client.GetJWTToken( ls_Url, ls_PostData, ls_Response )
If li_Return = 1 and Pos ( ls_Response, "access_token" ) > 0 Then
 ljson_Parser = Create JsonParser
 ljson_Parser.LoadString(ls_Response)
 ls_TokenType = ljson_Parser.GetItemString("/token_type")
 ls_AccessToken = ljson_Parser.GetItemString("/access_token")
 //Application Set Authorization Header
 Getapplication().SetHttpRequesTheader("Authorization", ls_TokenType + " " +ls_AccessToken, true)
 //Set Global Variables
 gl_Expiresin = Long (ljson_Parser.GetItemString("/expires_in"))

 li_rtn = 1
Else
 MessageBox( "AccessToken Falied", "Return :" + String ( li_Return ) )
End If

If IsValid ( ljson_Parser ) Then DesTroy ( ljson_Parser )
If IsValid ( lrc_Client ) Then DesTroy ( lrc_Client )

Return li_rtn


Step 3: Insert a timing object (timing_1) to the application and add the following scripts to the Timer event of timing_1.

1) Open the application object and then select from menu Insert > Object > Timing to add a timing object to the application.

2) Add the following scripts to the Timer event of timing_1.

//Authorization
f_Authorization()


Step 4: Add the following scripts to the application Open event.

Place the scripts before the database connection is established.

These scripts create the user sessions manually (using the BeginSession function) and get the token information into the session.

//Authorization
If f_Authorization() <> 1 Then
 Return
End If

//StartSession
long ll_return
Try
 ll_return = Beginsession()
 If ll_return <> 0 Then
  Messagebox("Beginsession Failed:" + String(ll_return), GetHttpResponseStatusText())
 End if
Catch ( Throwable ex)
 MessageBox( "Throwable", ex.GetMessage())
 Return
End Try

//Refresh Token for timing
If gl_Expiresin > 0 And (gl_Expiresin - gl_ClockSkew) > 0 Then
 //Timer = Expiresin - ClockSkew 
 //7200 - 3
 timing_1.Start(gl_Expiresin - gl_ClockSkew)
End If

// Connect to db

// Profile PB Postgres V2021
//SQLCA.DBMS = "ODBC"
//SQLCA.AutoCommit = False
//SQLCA.DBParm = "ConnectString='DSN=PB Postgres V2021',TrimSpaces='Yes'"
//Connect Using SQLCA;


Step 5: Add the following scripts to the SystemError event.

These scripts will throw the error message when there are session, license or token errors, or authorize the token again when the token is invalid or expired.

Choose Case error.Number
 Case 220 to 229 //Session Error
  MessageBox ("Session Error", "Number:" + String(error.Number) + "~r~nText:" + error.Text )
 Case 230 to 239 //License Error
  MessageBox ("License Error", "Number:" + String(error.Number) + "~r~nText:" + error.Text )
 Case 240 to 249 //Token Error
  MessageBox ("Token Error", "Number:" + String(error.Number) + "~r~nText:" + error.Text )
  //Authorization
  f_Authorization()
 Case Else
  MessageBox ("SystemError", "Number:" + String(error.Number) + "~r~nText:" + error.Text )
End Choose


Add an INI file

Create the following INI file in the same location as the PBT file and name it CloudSetting.ini.

The INI file specifies the Cognito Web API URL.

[Setup]
OauthURL=https://localhost:5001/Cognito/getToken
Start session manually by code

By default the user session is automatically created when the application starts; and the session includes no token. To enable the session to include the token, we will need to start the session manually instead of automatically.

To start the session manually,

Step 1: Enable "Begin session by code" in the PowerBuilder IDE. (Steps: Open the application object painter, click Additional Properties in the application's Properties dialog; in the Application dialog, select the PowerServer tab and then select the Begin session by code option.)

Step 2: Call the BeginSession function in the application Open event. (See the scripts added in step 4 in the section "Add scripts".)


Modify and re-deploy the PowerServer project

Step 1: Add the INI file CloudSetting.ini to the Files preloaded in uncompressed format section under the External Files tab.

Step 2: Select RESTClient Support and Compression Support under the Runtime tab.


Step 3: Specify the URL of the PowerServer Web API in the Web APIs tab. Make sure the port number is not occupied by any other program.

Tip: You can execute the command "netstat -ano | findstr 5009" to check if the port number is occupied by any other program.


Step 4: Save the changes and deploy the PowerServer project (using the "Build & Deploy PowerServer Project" option) so that the above settings can take effect in the installable cloud app.

Task 3: Modify the PowerServer Web API

Purpose

This task modifies the PowerServer Web API to achieve the following results:

  • Validates the identity token, and (if validation is successful) gets data from the database.

    The validation will be performed within the PowerServer Web API.

Instructions

Note

The PowerServer Web API solution will be restored to default when it gets updated from the PowerBuilder IDE. Therefore, make sure to make the following changes every time after the solution is updated or re-created.

Step 1: Open the PowerServer Web API solution (PowerServerApp.sln) in SnapDevelop. This solution is created when building the PowerServer project in PowerBuilder IDE.

Step 2: Edit the appsettings.json file to enable the authentication service and specify the AWS Cognito user pool.

1) Modify the value of "UsingAuthentication" to true to enable the authentication service.

"UsingAuthentication": true,

2) Specify the AWS Cognito user pool that will be used to validate the identity token.

"AWS": {
    "Region": "us-west-2",
    "UserPoolId": "us-west-2_5wyOzYn1d",
    "UserPoolClientId": "4linbauf6d58b552r6lc3gbpkc",
    "UserPoolClientSecret": "1prlm08gm3aptlokcbai88ekiegff9mqbc98nhebfart5g4a3cr2"
  }


Step 3: Install the following NuGet packages: Amazon.AspNetCore.Identity.Cognito and Amazon.Extensions.CognitoAuthentication.

Step 4: Add a folder named Cognito, and add the following files to the folder.

1) Add a class file named CognitoTokenValidator.cs that includes the following scripts:

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;

namespace ServerAPIs
{
    public class CognitoTokenValidator : ISecurityTokenValidator
    {
        private readonly JwtSecurityTokenHandler _jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
        
        public string RequiredClientId { get; set; }
        public string RequiredScope { get; set; }
        public string Authority { get; set; }
        
        public bool CanReadToken(string securityToken)
        {
            return _jwtSecurityTokenHandler.CanReadToken(securityToken);
        }
        
        public ClaimsPrincipal ValidateToken
        (
            string securityToken,
            TokenValidationParameters validationParameters,
            out SecurityToken validatedToken
        )
        {
            var principles =
                _jwtSecurityTokenHandler.ValidateToken(securityToken, validationParameters, out var jwtValidatedToken);
                
            if (RequiredClientId != null &&
                !principles.HasClaim(_ =>
                    _.Type == "client_id" &&
                    _.Value == RequiredClientId &&
                    (Authority == null || _.Issuer == Authority)) ||
                RequiredScope != null &&
                !principles.HasClaim(_ =>
                    _.Type == "scope" &&
                    _.Value.Split(',').Contains(RequiredScope)))
            {
                principles = new ClaimsPrincipal();
                validatedToken = null;
            }
            
            var roleClaims = principles.Claims
                .Where(t => t.Type == "cognito:groups")
                .Select(claim => new Claim(ClaimTypes.Role, claim.Value));
            principles.AddIdentity(new ClaimsIdentity(roleClaims));
            
            validatedToken = jwtValidatedToken;
            
            return principles;
        }
        
        public bool CanValidateToken => _jwtSecurityTokenHandler.CanValidateToken;
        
        public int MaximumTokenSizeInBytes { get; set; } = TokenValidationParameters.DefaultMaximumTokenSizeInBytes;
    }
}

2) Add a class file named CognitoJwtBearerOptions.cs that includes the following scripts:

using Microsoft.AspNetCore.Authentication.JwtBearer;

namespace ServerAPIs
{
    public class CognitoJwtBearerOptions
    {
        public CognitoJwtBearerOptions(JwtBearerOptions jwtBearerOptions)
        {
            JwtBearerOptions = jwtBearerOptions;
        }
        
        public JwtBearerOptions JwtBearerOptions { get; set; }
        public string ClientId { get; set; }
        public string IdPUrl { get; set; }
    }
}

3) Add a class file named JwtBearerExtensions.cs that includes the following scripts:

using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;

namespace ServerAPIs
{
    public static class JwtBearerExtensions
    {
        public static AuthenticationBuilder AddCognitoJwtBearer(
            this AuthenticationBuilder builder, IConfiguration configuration)
        {
            void action(JwtBearerOptions options)
            {
                var cognitoJwtBearerOptions = new CognitoJwtBearerOptions(options)
                {
                    ClientId = configuration.GetAWSCognitoClientOptions().UserPoolClientId,
                    IdPUrl = $"https://cognito-idp.{configuration.GetAWSOptions().Region.SystemName}.amazonaws.com/{configuration.GetAWSCognitoClientOptions().UserPoolId}"
                };
                cognitoJwtBearerOptions.JwtBearerOptions.RequireHttpsMetadata = false;
                
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateAudience = false
                };
                
                var existingJwtSecurityTokenHandler = options.SecurityTokenValidators
                    .FirstOrDefault(_ => _.GetType() == typeof(JwtSecurityTokenHandler));
                if (existingJwtSecurityTokenHandler != null)
                {
                    options.SecurityTokenValidators.Remove(existingJwtSecurityTokenHandler);
                }
                
                var awsTokenValidator = new CognitoTokenValidator
                {
                    RequiredClientId = cognitoJwtBearerOptions.ClientId,
                    Authority = cognitoJwtBearerOptions.IdPUrl
                };
                
                options.SecurityTokenValidators.Add(awsTokenValidator);
                options.Authority = cognitoJwtBearerOptions.IdPUrl;
            }
            
            builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, action);
            
            return builder;
        }
    }
}

Step 5: Modify the Startup.cs class file to add Cognito as the identity provider.

1) Add the following namespace:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using ServerAPIs;

2) Add Cognito as the identity provider and comment out the default authentication scripts.

            // Adds Amazon Cognito as Identity Provider
            services.AddCognitoIdentity();
            services.AddAuthentication(option =>
            {
                option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddCognitoJwtBearer(Configuration);


Step 6: Save the changes and click Run from the SnapDevelop toolbar to start the PowerServer Web API. Make sure the console window displays "Application started...".

Now that the configuration has completed, you can start the Cognito Web API and the PowerServer Web API. If these two Web APIs are running on the same machine, make sure their port numbers do not conflict with each other. You can modify the port number in the launchSettings.json file.

After that, you can run the installable cloud app, and view the logs in the API console windows to make sure the authorization is successful.