# Amazon Cognito Authentication with Hosted UI for ASP.NET  Core Apps

In our previous article, [Authentication with Amazon Cognito and ASP.NET Core Identity](https://blog.raulnq.com/authentication-with-amazon-cognito-and-aspnet-core-identity), we discussed how to authenticate ASP.NET applications using the Amazon Cognito user pools API. This approach required us to maintain a significant amount of integration-related code. Luckily, there's another option: [Hosted UI and federation endpoints](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-integration.html). Amazon Cognito Hosted UI is a fully managed user interface for handling user authentication. It allows developers to easily add user sign-up, sign-in, and account management functionality to their applications without the need to build and maintain their authentication UI.

Our starting point will be the application built earlier, which we can find [here](https://github.com/raulnq/aws-cognito/tree/main). Let's start updating the `template.yaml` file as follows:

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  SAM Template

Resources:
  UserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UsernameAttributes:
       - email
      UsernameConfiguration: 
        CaseSensitive: false
      Policies:
        PasswordPolicy:
         MinimumLength: 8
         RequireLowercase: false
         RequireNumbers: false
         RequireSymbols: false
         RequireUppercase: false
         TemporaryPasswordValidityDays: 7
      MfaConfiguration: 'OFF'
      AccountRecoverySetting:
        RecoveryMechanisms:
          - Name: verified_email
            Priority: 1
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: false
      AutoVerifiedAttributes:
        - email
      UserPoolName: "myuserpool"
      UserAttributeUpdateSettings:
        AttributesRequireVerificationBeforeUpdate:
          - email 
      Schema:
        - Name: email
          AttributeDataType: String
          Mutable: false
          Required: true
      EmailConfiguration:
        EmailSendingAccount: COGNITO_DEFAULT

  UserPoolClient:
    Type: "AWS::Cognito::UserPoolClient"
    Properties:
      ClientName: "myclient"
      GenerateSecret: true
      UserPoolId: !Ref UserPool
      ExplicitAuthFlows:
        - ALLOW_USER_SRP_AUTH
        - ALLOW_REFRESH_TOKEN_AUTH 
      SupportedIdentityProviders:
        - COGNITO
      AllowedOAuthFlowsUserPoolClient: true
      AllowedOAuthFlows:
        - code
      AllowedOAuthScopes:
        - email
        - openid
        - profile
        - aws.cognito.signin.user.admin
      LogoutURLs:
        - "https://localhost:7119/Account/Loggedout" 
      CallbackURLs:
        - "https://localhost:7119/signin-oidc"

  UserDomainPool:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      Domain: "myuserdomainpoolx95"
      UserPoolId: !Ref UserPool

Outputs:
  CognitoUserPoolID:
    Value: !Ref UserPool
    Description: The UserPool ID
  CognitoClientID:
    Value: !Ref UserPoolClient
    Description: The app client
  HostedUIDomain:
    Value: !Ref UserDomainPool
    Description: Hosted UI domain
```

From the original file, we modified the `AWS::Cognito::UserPoolClient` resource:

* `AllowedOAuthFlowsUserPoolClient`: Enables [OAuth](https://datatracker.ietf.org/doc/html/rfc6749) features in our user pool app client.
    
* `AllowedOAuthFlows`: Specifies which [OAuth grant types](https://docs.aws.amazon.com/cognito/latest/developerguide/federation-endpoints-oauth-grants.html) are allowed for the user pool client. An OAuth grant type defines how client applications interact with the server to obtain access tokens and authenticate users. The supported OAuth grant types are:
    
    * `code`: This type involves the client getting an authorization code from the server once the user is authenticated. The client then trades this code for an [ID token](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-id-token.html), an [access token](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-access-token.html), and a [refresh token](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-refresh-token.html). It's a good fit for web applications that can safely keep the client secret. Find more information [here](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow).
        
    * `implicit`: In this type, the client gets an [ID token](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-id-token.html) and an [access token](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-access-token.html) directly from the server once the user is authenticated. This method is often used in single-page applications (SPAs) where it's not possible to securely keep the client secret. Find more information [here](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-implicit-grant-flow).
        
    * `client_credentials`: The client application trades its credentials (client ID and client secret) for an [access token](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-access-token.html). This method is for machine-to-machine authentication where no user is involved. Find more information [here](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow).
        
* `AllowedOAuthScopes`**:** Defines the OAuth [scopes](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-define-resource-servers.html) that the client is allowed to request. Each scope represents a specific permission or set of permissions the client can request:
    
    * Values provided by OAuth are `phone`, `email`, `openid`, and `profile`.
        
    * The value provided by AWS is `aws.cognito.signin.user.admin`.
        
    * Custom scopes created in [Resource Servers](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-define-resource-servers.html#cognito-user-pools-define-resource-servers-about-resource-servers) are also supported.
        
* `CallbackURLs`: A list of allowed redirect URIs. Each redirect URI must be an absolute URI using the HTTPS protocol, except for localhost.
    
* `LogoutURLs`: A list of allowed logout URLs.
    

A new resource, `AWS::Cognito::UserPoolDomain`, was created. It represents the domain associated with our user pool, hosting sign-up, and sign-in pages for our application. It's time to update our application. Run the following commands to update the resources:

```yaml
sam build
sam deploy --guided
```

Let's update the application. Run the following command:

```powershell
dotnet add MyWebApp package Microsoft.AspNetCore.Authentication.OpenIdConnect
```

Open the solution and update the `Program.cs` file as follows:

```csharp
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

var builder = WebApplication.CreateBuilder(args);

var configuration = builder.Configuration;

builder.Services.AddControllersWithViews();

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.MetadataAddress = $"https://cognito-idp.{configuration["AWS:Region"]}.amazonaws.com/{configuration["AWS:UserPoolId"]}/.well-known/openid-configuration";
    options.ClientId = configuration["AWS:UserPoolClientId"];
    options.ClientSecret = configuration["AWS:UserPoolClientSecret"];
    options.UsePkce = true;
    options.Events = new OpenIdConnectEvents()
    {
        OnRedirectToIdentityProviderForSignOut = OnRedirectToIdentityProviderForSignOut
    };
    options.Scope.Clear();
    options.Scope.Add("openid");
    options.Scope.Add("email");
    options.Scope.Add("aws.cognito.signin.user.admin");
    options.Scope.Add("profile");
    options.SaveTokens = true;

    Task OnRedirectToIdentityProviderForSignOut(RedirectContext context)
    {
        context.ProtocolMessage.Scope = "openid";
        context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.Code;
        var cognitoDomain = $"https://{configuration["AWS:Domain"]}.auth.{configuration["AWS:Region"]}.amazoncognito.com" ;
        var clientId = configuration["AWS:UserPoolClientId"]; ;
        var logoutUrl = $"{context.Request.Scheme}://{context.Request.Host}{configuration["AWS:AppSignOutUrl"]}";
        context.ProtocolMessage.IssuerAddress = $"{cognitoDomain}/logout?client_id={clientId}&logout_uri={logoutUrl}&redirect_uri={logoutUrl}";
        context.Properties.Items.Remove(CookieAuthenticationDefaults.AuthenticationScheme);
        context.Properties.Items.Remove(OpenIdConnectDefaults.AuthenticationScheme);
        return Task.CompletedTask;
    }
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

The `AddOpenIdConnect` method sets up the Open ID Connect authentication middleware:

* `MetadataAddress`: OpenID Connect defines a discovery mechanism called [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html), where an OpenID server publishes its metadata at a well-known URL, typically `https://{server}/.well-known/openid-configuration`. This URL returns a JSON listing of the OpenID/OAuth endpoints, supported scopes and claims, public keys used to sign the tokens and other details. The clients can use this information to construct a request to the OpenID server. We can use this property instead of the `Authority` property.
    
* `ClientId`: The client ID assigned to your application by the OpenID Connect provider.
    
* `ResponseType`: Specifies the type of response expected from the server. Values include `code` for authorization code grant flow and `id_token token` for implicit grant flow.
    
* `ClientSecret`: The client secret assigned to your application by the OpenID Connect provider. This is used for securely exchanging tokens and authenticating requests.
    
* `Scopes`: Specifies the scopes requested by your application during the authentication process. Scopes include `openid`, `profile`, `email`, etc.
    
* `Events`: Allows you to handle various events during the authentication. For example, the `OnRedirectToIdentityProviderForSignOut` is triggered when the application needs to redirect the user to the identity provider for sign-out. We can use this event to customize the redirect URL or to add additional parameters to the sign-out request.
    
* `SaveTokens`: When set to `true`, this property instructs the OpenID Connect middleware to save the tokens received from the identity provider (such as access token, ID token, and refresh token) in the authentication properties associated with the user's claims principal.
    
* `UsePkce`**:**[Proof Key for Code Exchange](https://oauth.net/2/pkce/) is a security extension to the Authorization Code flow to prevent certain types of attacks. When set to `true`, this property instructs the OpenID Connect middleware to use PKCE during the Authorization Code flow.
    

Open the `appsettings.json` file to update its content as follows:

```json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "AWS": {
    "Region": "<MY_REGION>",
    "UserPoolClientId": "<MY_CLIENT_ID>",
    "UserPoolClientSecret": "<MY_CLIENT_SECRET>",
    "UserPoolId": "<MY_USER_POOL_ID>",
    "Domain": "myuserdomainpoolx95",
    "AppSignOutUrl": "/Account/Loggedout"
  }
}
```

Open the `Controllers/AccountController.cs` file and update the content with:

```csharp
public class AccountController : Controller
{
    public AccountController()
    {
    }

    [HttpGet]

    public IActionResult Logout()
    {
        return SignOut(CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme);
    }

    [HttpGet]
    public IActionResult Loggedout()
    {
        return View();
    }

    [HttpGet]
    public IActionResult Login(string returnUrl="/")
    {
        var props = new AuthenticationProperties()
        {
            RedirectUri = returnUrl,
        };
        return Challenge(props, OpenIdConnectDefaults.AuthenticationScheme);
    }
}
```

* The `Login` method initiates the authentication process by returning the `Challenge` method. After Amazon Cognito redirects to `https://localhost:7119/signin-oidc`, the application will be redirected to the URL specified in the `RedirectUri` property.
    
* The `Logout` method is used to trigger the logout of the current user by invalidating their authentication session.
    
* The `Loggedout` method is the endpoint where Amazon Cognito redirects after a logout (`https://localhost:7119/Account/Loggedout`). A default endpoint `https://localhost:7119/signout-callback-oidc` can be used, but it will show an empty page after the redirect.
    

Add a new `Views/Account/Loggedout.cshtml` file as follows:

```xml
@{
    ViewData["Title"] = "Logged out";
}

<h2>@ViewData["Title"]</h2>
<p>
    You have successfully logged out.
</p>
```

Update the `Views/Shared/_Layout.cshtml` file with the content below:

```xml
@using Microsoft.AspNetCore.Identity
@using Amazon.Extensions.CognitoAuthentication
@using System.Security.Claims

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - MyWebApp</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/MyWebApp.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">MyWebApp</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                    </ul>
                    <ul class="navbar-nav">
                        @if (User.Identity.IsAuthenticated)
                        {
                            <li class="nav-item">
                                <span class="nav-link text-dark">Hello @User.FindFirstValue(ClaimTypes.Email)!</span>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Logout">Logout</a>
                            </li>
                        }
                        else
                        {
                            <li class="nav-item">
                                <a class="nav-link text-dark" id="login" asp-area="" asp-controller="Account" asp-action="Login" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })">Login</a>
                            </li>
                        }
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2024 - MyWebApp - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
```

Update the `Views/Home/Index.cshtml` file with the content below:

```xml
﻿@using Microsoft.AspNetCore.Authentication
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>

    @if (User.Identity.IsAuthenticated)
    {
        <p>
            Authenticated!
        </p>
        <h2>Claims</h2>
        <table class="table table-bordered">
            <tr>
                <th>Claim Type</th>
                <th>Value</th>
            </tr>
            @foreach (var item in User.Claims)
            {
                <tr>
                    <td>
                        @item.Type
                    </td>
                    <td>
                        @item.Value
                    </td>
                </tr>
            }
        </table>
    }
</div>
```

Finally, delete the `Models/ConfirmViewModel.cs`, `Models/LoginViewModel.cs`, `Models/RegisterViewModel.cs`, `Views/Account/Confirm.cshtml`, `Views/Account/Login.cshtml`, and `Views/Account/Register.cshtml` files. Then, run the application:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711045461593/ad43455b-b0ed-498c-b22c-16f2aff3c847.png align="center")

Click the login button, and the browser will redirect us to the Amazon Cognito login page:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711045537749/b8d36d36-293b-4fde-aaa3-f55f92a2dd6f.png align="center")

After logging in, Amazon Cognito will redirect us back to the application:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711045582281/c13bc0ee-2101-45e6-9ee9-66e96c743141.png align="center")

Amazon Cognito Hosted UI makes adding user authentication simpler, providing a secure, customizable, and scalable solution. It cuts down on development time and effort, improving the overall user experience of our applications. All the code can be found [here](https://github.com/raulnq/aws-cognito/tree/hosted-ui). Thanks, and happy coding.
