Un esempio di come mettere in sicurezza delle Web API sviluppate con .NET 4.8.1 proteggendole con Azure Client Credential Flow.

Registrazione su Azure

Primo step creare su Azure una nuova App Registration
New App Registration
New App Registration

creare un client secret
New Client Secret
New Client Secret
Copiarlo subito perchè successivamente non sarà più accessibile .
aggiungere lo scope di default
Scope
Scope
nella shermata di riepilogo si possono trovale le altre informazioni quali Tenant ID, Client ID e Scope (Application ID Uri)
Scope
Scope

Web API

Creare un nuovo progetto con l'autenticazione
Template Web ASP.NET Framework
Template Web ASP.NET Framework

scegliendo il .NET Framework 4.8
Scelgo il .NET 4.8.1
Scelgo il .NET 4.8.1

scegli come tipo di progetto API Web e l'autenticazione Microsoft Identity Platform
Authenticazione Microsoft Identity Platform
Authenticazione Microsoft Identity Platform

A questo punto si apre un dialogo dove puoi scegliere l'App Registration su Azure
Scelgo l'APP Registration
Scelgo l'APP Registration

verifica e conferma le scelte
Riepilogo
Riepilogo

Passaggi manuali

Se non si vuole fare la configurazione con il wizard, questi sono gli step necessari

Pacchetti Nuget necessari

XML: packages.config

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Microsoft.AspNet.WebApi" version="5.2.9" targetFramework="net481" />
  <package id="Microsoft.AspNet.WebApi.Client" version="5.2.9" targetFramework="net481" />
  <package id="Microsoft.AspNet.WebApi.Core" version="5.2.9" targetFramework="net481" />
  <package id="Microsoft.AspNet.WebApi.WebHost" version="5.2.9" targetFramework="net481" />
  <package id="Microsoft.Bcl.AsyncInterfaces" version="7.0.0" targetFramework="net481" />
  <package id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="3.6.0" targetFramework="net481" />
  <package id="Microsoft.IdentityModel.Abstractions" version="6.27.0" targetFramework="net481" />
  <package id="Microsoft.IdentityModel.JsonWebTokens" version="6.27.0" targetFramework="net481" />
  <package id="Microsoft.IdentityModel.Logging" version="6.27.0" targetFramework="net481" />
  <package id="Microsoft.IdentityModel.Protocol.Extensions" version="1.0.4.403061554" targetFramework="net481" />
  <package id="Microsoft.IdentityModel.Protocols" version="6.27.0" targetFramework="net481" />
  <package id="Microsoft.IdentityModel.Protocols.OpenIdConnect" version="6.27.0" targetFramework="net481" />
  <package id="Microsoft.IdentityModel.Protocols.WsFederation" version="6.27.0" targetFramework="net481" />
  <package id="Microsoft.IdentityModel.Tokens" version="6.27.0" targetFramework="net481" />
  <package id="Microsoft.IdentityModel.Tokens.Saml" version="6.27.0" targetFramework="net481" />
  <package id="Microsoft.IdentityModel.Xml" version="6.27.0" targetFramework="net481" />
  <package id="Microsoft.Owin" version="4.2.2" targetFramework="net481" />
  <package id="Microsoft.Owin.Host.SystemWeb" version="4.2.2" targetFramework="net481" />
  <package id="Microsoft.Owin.Security" version="4.2.2" targetFramework="net481" />
  <package id="Microsoft.Owin.Security.ActiveDirectory" version="4.2.2" targetFramework="net481" />
  <package id="Microsoft.Owin.Security.Jwt" version="4.2.2" targetFramework="net481" />
  <package id="Microsoft.Owin.Security.OAuth" version="4.2.2" targetFramework="net481" />
  <package id="Microsoft.Owin.Security.OpenIdConnect" version="4.2.2" targetFramework="net481" />
  <package id="Microsoft.Web.Infrastructure" version="2.0.1" targetFramework="net481" />
  <package id="Newtonsoft.Json" version="13.0.2" targetFramework="net481" />
  <package id="Owin" version="1.0" targetFramework="net481" />
  <package id="System.Buffers" version="4.5.1" targetFramework="net481" />
  <package id="System.IdentityModel.Tokens.Jwt" version="6.27.0" targetFramework="net481" />
  <package id="System.Memory" version="4.5.5" targetFramework="net481" />
  <package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net481" />
  <package id="System.Runtime.CompilerServices.Unsafe" version="6.0.0" targetFramework="net481" />
  <package id="System.Text.Encoding" version="4.3.0" targetFramework="net481" />
  <package id="System.Text.Encodings.Web" version="7.0.0" targetFramework="net481" />
  <package id="System.Text.Json" version="7.0.2" targetFramework="net481" />
  <package id="System.Threading.Tasks.Extensions" version="4.5.4" targetFramework="net481" />
  <package id="System.ValueTuple" version="4.5.0" targetFramework="net481" />
</packages>

lo Startup.cs diventa

C#: Startup.cs

using Owin;

namespace WebApplication1
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }
    }
}

Nella Startup.Auth.cs, definisco l'autenticazione tramite Token JWT

C#: Startup.Auth.cs

using System.Configuration;
using Microsoft.IdentityModel.Tokens; 
using Microsoft.Owin.Security.ActiveDirectory;
using Owin;

namespace WebApplication1
{
    public partial class Startup
    {
        // Per altre informazioni su come configurare l'autenticazione, vedere https://go.microsoft.com/fwlink/?LinkId=301864
        public void ConfigureAuth(IAppBuilder app)
        {
            app.UseWindowsAzureActiveDirectoryBearerAuthentication(
                new WindowsAzureActiveDirectoryBearerAuthenticationOptions
                {
                    Tenant = ConfigurationManager.AppSettings["ida:TenantId"],
                    TokenValidationParameters = new TokenValidationParameters {
                         ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
                    },
                });
        }
    }
}
i paramtri di configurazione per validare il token JWT sono nel file web.config

XML: web.config

<configuration>
	<appSettings>
		<add key="ida:TenantId" value="b32dxxxx-xxxx-xxxx-xxxx-xxxxxxxxdca9" />
		<add key="ida:Audience" value="api://5881xxxx-xxxx-xxxx-xxxx-xxxxxxxx03e3" />
	</appSettings>
	...
<configuration>
Nel parametro ida:Audience indico solo lo scope senza nessun suffisso.
Il parametro ida:Audience deve corrisponde al valore aud presente nel token JWT
JWF Scope/Audience
JWF Scope/Audience

Esempio di controller protetto tramite la decorazione [Autorize] che sarà accessibile solo con un token JWT valido

C#: ValuesController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace WebApplication1.Controllers
{
    [Authorize]
    public class ValuesController : ApiController
    {
        // GET api/values
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        public string Get(int id)
        {
            return "value";
        }

        // POST api/values
        public void Post([FromBody]string value)
        {
        }

        // PUT api/values/5
        public void Put(int id, [FromBody]string value)
        {
        }

        // DELETE api/values/5
        public void Delete(int id)
        {
        }
    }
}

Test

Per testare il funzionamento si può usare questa console application

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Net.Http;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace ConsoleApp1
{
    class Program
    {
        static async Task Main(string[] args)
        {
            string tenantId = "b32dxxxx-xxxx-xxxx-xxxx-xxxxxxxxdca9";
            string clientId = "5881xxxx-xxxx-xxxx-xxxx-xxxxxxxx03e3";
            string clientSecret = "FAFxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxadV";
            string scope = "api://5881xxxx-xxxx-xxxx-xxxx-xxxxxxxx03e3/.default";

            string token = await GetTokenAsync(tenantId, clientId, clientSecret, scope);

            string result = await CallApi(token);

            Console.WriteLine(result);

        }
Per eseguire la chiamata alla API protette serviranno i valori di: tenantId, clientId, secret e scope.
Nella variabile scope deve essere aggiunto il suffisso /.default.

Tramite il ClientID e il ClientSecret richiedo il token JWT di autenticazione da usare nelle chiamte alle API

C#: Esempio GetToken

private static async Task<string> GetTokenAsync(string tenantId, string clientId, string clientSecret, string scope)
{
    // endpoint di autenticazione per ottenere il token
    string tokenUrl = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token";

    var content = new FormUrlEncodedContent(new Dictionary<string, string> {
            { "client_id",clientId },
            { "client_secret", clientSecret},
            { "grant_type", "client_credentials" },
            { "scope", scope },
        });

    using (var httpClient = new HttpClient())
    using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri(tokenUrl)))
    {
        httpRequestMessage.Content = content;

        using (var response = await httpClient.SendAsync(httpRequestMessage))
        {
            if (response.IsSuccessStatusCode)
            {
                var responseStream = await response.Content.ReadAsStringAsync();
                Console.WriteLine(responseStream);
                var t = JsonConvert.DeserializeObject<TokenResponse>(responseStream);

                // ho il token JWT necessario per le chiamate verso le API protette
                return t.AccessToken;
            }
            else
            {
                throw new Exception(response.ReasonPhrase);
            }
        }
    }

}
a questo punto posso chiamare l'API protetta, aggiungendo nell'header della chiamata HTTP la riga:
Authentication: Bearer {header].{payload}.{signature}
questo è l'esempio di chiamata in C#

C#: Esempio CallApi

private static async Task<string> CallApi(string token)
    {
        string apiUrl = $"https://localhost:44369/api/values";


        using (var httpClient = new HttpClient())
        {

            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

            using (var response = await httpClient.GetAsync(apiUrl))
            {
                if (response.IsSuccessStatusCode)
                {
                    var responseStream = await response.Content.ReadAsStringAsync();
                    Console.WriteLine(responseStream);

                    return responseStream;
                }
                else
                {
                    throw new Exception(response.ReasonPhrase);
                }
            }
        }
    }

Per la GetToken è necessaria questa classe di supporto

C#

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class TokenResponse
    {
        //"token_type"
        //{"token_type":"Bearer",
        //"expires_in":3599,
        //"ext_expires_in":3599,
        //"access_token"":"eyJ0eXAiOiJKV1

        [JsonProperty("token_type")]
        public string TokenType { get; set; }


        [JsonProperty("access_token")]
        public string AccessToken { get; set; }
    }
}
Tags:
C#240 ASP.NET54 .NET69 Azure8
Potrebbe interessarti anche: