Utilizzare Amazon Cognito in ASP.NET Core - UGIdotNET
Questo sito si serve dei cookie per fornire servizi. Utilizzando questo sito acconsenti all'utilizzo dei cookie. Ulteriori informazioni Ok

Utilizzare Amazon Cognito in ASP.NET Core

di pubblicato il 12/02/2025

.NET  .NET on AWS  Amazon Cognito  AWS 

Amazon Cognito permette di gestire utenze e flussi di autenticazione: in questo articolo spiegheremo come integrarlo nella nostra applicazione .NET.

I temi di autenticazione e autorizzazione sono sempre tra i più delicati da affrontare quando costruiamo le nostre applicazioni.

Quando si tratta di implementare questa parte ci si aprono diverse strade. Possiamo optare per una soluzione “home-made” sfruttando framework come ASP.NET Core Identity e salvando in un nostro storage le informazioni degli utenti, oppure possiamo decidere di delegare questa gestione ad un sistema esterno.

Nel caso di quest'ultima opzione i Cloud Provider offrono spesso delle soluzioni che ci permettono di configurare tutto il flusso di autenticazione e autorizzazione dei nostri utenti. In AWS questo servizio si chiama Cognito.

Amazon Cognito è un CIAM (Customer Identity Access Management), ovvero un sistema che permette di gestire le utenze e i flussi di autenticazione e autorizzazione per una applicazione. Uno dei grossi pregi è che da la possibilità di avere fino a 50.000 utenti attivi al mese in modo del tutto gratuito.

Configuriamo Amazon Cognito

Quando andiamo a provisionare una istanza di Amazon Cognito possiamo scegliere se creare uno User pool, un Identity pool oppure entrambi.

Uno User pool è una directory di utenti che permette di gestire questi ultimi, configurando i flussi di autenticazione e autorizzazione. È possibile configurare l'autenticazione per utilizzare diversi Identity provider, Multi-factor authentication e altro.

Identity pool è invece necessario quando si tratta di autorizzare l'accesso ai servizi di AWS da un nostro applicativo.

Ai fini dell'articolo andiamo a creare uno User pool. Possiamo lasciare i valori di default ma mettiamo tre paletti:

  • Permettiamo l'autenticazione utilizzando esclusivamente la mail dell'utente
  • Assicuriamoci che la Hosted UI sia disabilitata
  • Quando andremo a censire la nostra applicazione nello user pool assicuriamoci di creare anche un client secret

 

Creato il nostro user pool possiamo andare a creare la nostra applicazione .NET.

Come possiamo integrare Amazon Cognito in .NET?

Per cominciare creiamo un classico progetto ASP.NET Core, ad esempio che usa Razor Pages, evitando di aggiungere la gestione degli individual account.
Creato il progetto aggiungiamo da NuGet i package Amazon.AspNetCore.Identity.Cognito e Amazon.Extensions.CognitoAuthentication.

Nel nostro Program.cs possiamo registrare il necessario nel motore di dependency injection utilizzando la seguente istruzione:

builder.Services.AddCognitoIdentity();

Assicuriamoci inoltre che siano attivati i middleware di Authentication e Authorization.

Andiamo a questo punto a configurare il file appsettings.json per dire al nostro applicativo quale sia la risorsa a cui si deve collegare.
Andiamo quindi ad aggiungere la seguente sezione (i valori specificati sono a titolo di esempio):

"AWS": {
  "Region": "<eu-west-1>",
  "UserPoolId": "eu-west-1_xxxxxx",
  "UserPoolClientId": "xxxxxxxxxxxxxxxxxxxxxxxxx",
  "UserPoolClientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxx"
}

In questa sezione stiamo andando a specificare le seguenti informazioni:

  • Region è la region AWS in cui abbiamo provisionato la risorsa (ad esempio eu-west-1)
  • UserPoolId è l'identificativo dello user pool creato in precedenza ed è formato dalla region seguita da un codice alfanumerico (ad esempio eu-west-1_xxxxx)
  • UserPoolClientId e UserPoolClientSecret sono rispettivamente il client id e il client secret dell'applicazione creata nello user pool che utilizzeremo per autenticarci.

 

Ai fini di verificare la corretta valorizzazione dei claim dell'utente modifichiamo la pagina Index.cshtml come segue:

@page
@model IndexModel
@{
  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>
</div>
<hr />
<div>
  @if (User.Identity?.IsAuthenticated ?? false)
  {
    <ul>
    @foreach (var claim in User.Claims)
    {
      <li><strong>@claim.Type</strong>: @claim.Value</li>
    }
    </ul>
  }
</div>

A questo punto creiamo due razor page Register.cshtml e Confirm.cshtml per verificare il corretto flusso di registrazione dell'utente.

La pagina di Confirm è la più semplice e ha questa struttura:

@page
@model UGIdotNET.SpikeTime.AwsCognito.Pages.ConfirmModel
@{
}

<h2>@ViewData["Title"]</h2>

<div class="row">
  <div class="col-md-4">
    <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
      <h4>Confirm your new account.</h4>
      <hr />
      <div asp-validation-summary="All" class="text-danger"></div>
      <div class="form-group">
        <label asp-for="Input.Code"></label>
        <input asp-for="Input.Code" class="form-control" />
        <span asp-validation-for="Input.Code" class="text-danger"></span>
      </div>
      <button type="submit" class="btn btn-default">Confirm Account</button>
    </form>
  </div>
</div>

@section Scripts {
  <partial name="_ValidationScriptsPartial" />
}

Per quanto riguarda questa pagina andiamo a gestire la conferma dell'account nel code behind della pagina con il seguente handler:

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
  returnUrl = returnUrl ?? Url.Content("~/");
  if (ModelState.IsValid)
  {
    var userEmail = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;

    var user = await _userManager.FindByEmailAsync(userEmail);
    if (user == null)
    {
      return NotFound($"Unable to load user with ID '{userEmail}'.");
    }

    var result = await _userManager.ConfirmSignUpAsync(user, Input.Code, true);
    if (!result.Succeeded)
    {
      throw new InvalidOperationException($"Error confirming account for user with ID '{userEmail}':");
    }
    else<br/>     {
      return returnUrl != null ? LocalRedirect(returnUrl) : Page() as IActionResult;
    }
  }

  // If we got this far, something failed, redisplay form
  return Page();
}

Dove _userManager è una istanza della classe CognitoUserManager<CognitoUser> che viene iniettata nel costruttore della pagina.

Per quanto riguarda la pagina di Register aggiungiamo il form di registrazione al markup nel file razor:

@page
@model UGIdotNET.SpikeTime.AwsCognito.Pages.RegisterModel
@{
  ViewData["Title"] = "Spike time - AWS Cognito";
}

<h2>@ViewData["Title"]</h2>

<div class="row">
  <div class="col-md-4">
    <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
      <h4>Create a new account.</h4>
      <hr />
      <div asp-validation-summary="All" class="text-danger"></div>
      <div class="form-group">
        <label asp-for="Input.Email"></label>
        <input asp-for="Input.Email" class="form-control" />
        <span asp-validation-for="Input.Email" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Input.Password"></label>
        <input asp-for="Input.Password" class="form-control" />
        <span asp-validation-for="Input.Password" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Input.ConfirmPassword"></label>
        <input asp-for="Input.ConfirmPassword" class="form-control" />
        <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
      </div>
      <button type="submit" class="btn btn-default">Register</button>
    </form>
  </div>
</div>

@section Scripts {
  <partial name="_ValidationScriptsPartial" />
}

e poi andiamo ad implementare l'handler sulla post nel code behind in questo modo:

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
  returnUrl = returnUrl ?? Url.Content("~/");
  if (ModelState.IsValid)
  {
    var user = _pool.GetUser(Input.Email);
    user.Attributes.Add(CognitoAttribute.Email.AttributeName, Input.Email);
    user.Attributes.Add(CognitoAttribute.Profile.AttributeName, Input.Email);
    user.Attributes.Add(CognitoAttribute.Name.AttributeName, "Alberto");

    var result = await _userManager.CreateAsync(user, Input.Password);
    if (result.Succeeded)
    {
      await _signInManager.SignInAsync(user, isPersistent: false);

      return RedirectToPage("./Confirm");
    }
    foreach (var error in result.Errors)
    {
      ModelState.AddModelError(string.Empty, error.Description);
    }
  }

  // If we got this far, something failed, redisplay form
  return Page();
}

Come si può vedere dal codice è possibile creare l'istanza dell'utente partendo dallo user pool di Cognito e utilizzando l'email, arricchendo poi le informazioni tramite degli attributi che vanno specificati dipendentemente dalla configurazione su AWS (in questo caso gli attributi necessari sono Email, Profile e Name). Recuperato l'utente posso utilizzare l'istanza della classe CognitoUserManager per creare l'elemento nella directory di AWS Cognito.

A questo punto, aprendo la home page del nostro sito possiamo verificare che l'Identity sia stata creata correttamente e che ci troviamo quindi in presenza dell'utente autenticato.

Conclusione

In questo tip abbiamo visto come è possibile integrare il servizio AWS Cognito all'interno della nostra applicazione ASP.NET Core utilizzando l'SDK ufficiale fornito da AWS.

Abbiamo visto come, utilizzando le classi di CognitoUserManager e la classica SignInManager che siamo abituati ad usare con ASP.NET Core Identity, sia possibile implementare il flusso di registrazione e di autenticazione di un utente direttamente nella nostra UI.

Il codice mostrato in questo esempio è disponibile su GitHub a questo link: SpikeTime/UGIdotNET.SpikeTime.AwsCognito.