Partager via


Sécuriser la communication entre l’hébergement et les intégrations de client

Cet article est une suite de deux articles précédents illustrant la création d’intégrations d’hébergement personnalisées et intégrations personnalisées client.

L'un des principaux avantages de .NET.NET Aspire est de simplifier la configurabilité des ressources et l'utilisation par les clients (ou les intégrations). Cet article montre comment partager des informations d’identification d’authentification à partir d’une ressource personnalisée dans une intégration d’hébergement, à l'client consommatrice dans une intégration de client personnalisée. La ressource personnalisée est un conteneur MailDev qui autorise les informations d’identification entrantes ou sortantes. L'intégration personnalisée client est un client MailKit qui envoie des emails.

Conditions préalables

Étant donné que cet article continue à partir du contenu précédent, vous devez avoir déjà créé la solution résultante comme point de départ pour cet article. Si ce n’est déjà fait, complétez les articles suivants :

La solution résultante de ces articles précédents contient les projets suivants :

  • MailDev. Hébergement: contient le type de ressource personnalisé pour le conteneur MailDev.
  • MailDevResource.AppHost: l’hôte d’application qui utilise la ressource personnalisée et la définit comme une dépendance pour un service Newsletter.
  • MailDevResource.NewsletterService: projet d’API web ASP.NET Core qui envoie des e-mails à l’aide du conteneur MailDev.
  • MailDevResource.ServiceDefaults: contient les configurations de service par défaut destinées au partage.
  • MailKit.Client: contient l'intégration de client personnalisée qui rend accessible le SmtpClient MailKit via une fabrique.

Mettre à jour la ressource MailDev

Pour transmettre les informations d’identification d’authentification de la ressource MailDev à l’intégration de MailKit, vous devez mettre à jour la ressource MailDev pour inclure les paramètres de nom d’utilisateur et de mot de passe.

Le conteneur MailDev prend en charge l’authentification de base pour le protocole SMTP (entrant et sortant). Pour configurer les informations d'identification entrantes, vous devez définir les variables d’environnement MAILDEV_INCOMING_USER et MAILDEV_INCOMING_PASS. Pour plus d’informations, consultez MailDev: Utilisation. Mettez à jour le fichier MailDevResource.cs dans le projet MailDev.Hosting, en remplaçant son contenu par le code C# suivant :

// For ease of discovery, resource types should be placed in
// the Aspire.Hosting.ApplicationModel namespace. If there is
// likelihood of a conflict on the resource name consider using
// an alternative namespace.
namespace Aspire.Hosting.ApplicationModel;

public sealed class MailDevResource(
    string name,
    ParameterResource? username,
    ParameterResource password)
        : ContainerResource(name), IResourceWithConnectionString
{
    // Constants used to refer to well known-endpoint names, this is specific
    // for each resource type. MailDev exposes an SMTP and HTTP endpoints.
    internal const string SmtpEndpointName = "smtp";
    internal const string HttpEndpointName = "http";

    private const string DefaultUsername = "mail-dev";

    // An EndpointReference is a core .NET Aspire type used for keeping
    // track of endpoint details in expressions. Simple literal values cannot
    // be used because endpoints are not known until containers are launched.
    private EndpointReference? _smtpReference;

    /// <summary>
    /// Gets the parameter that contains the MailDev SMTP server username.
    /// </summary>
    public ParameterResource? UsernameParameter { get; } = username;

    internal ReferenceExpression UserNameReference =>
        UsernameParameter is not null ?
        ReferenceExpression.Create($"{UsernameParameter}") :
        ReferenceExpression.Create($"{DefaultUsername}");

    /// <summary>
    /// Gets the parameter that contains the MailDev SMTP server password.
    /// </summary>
    public ParameterResource PasswordParameter { get; } = password;

    public EndpointReference SmtpEndpoint =>
        _smtpReference ??= new(this, SmtpEndpointName);

    // Required property on IResourceWithConnectionString. Represents a connection
    // string that applications can use to access the MailDev server. In this case
    // the connection string is composed of the SmtpEndpoint endpoint reference.
    public ReferenceExpression ConnectionStringExpression =>
        ReferenceExpression.Create(
            $"Endpoint=smtp://{SmtpEndpoint.Property(EndpointProperty.Host)}:{SmtpEndpoint.Property(EndpointProperty.Port)};Username={UserNameReference};Password={PasswordParameter}"
        );
}

Ces mises à jour ajoutent une propriété UsernameParameter et PasswordParameter. Ces propriétés sont utilisées pour stocker les paramètres de l'MailDev nom d’utilisateur et mot de passe. La propriété ConnectionStringExpression est mise à jour pour inclure les paramètres de nom d’utilisateur et de mot de passe dans la chaîne de connexion. Ensuite, mettez à jour le fichier MailDevResourceBuilderExtensions.cs dans le projet MailDev.Hosting avec le code C# suivant :

using Aspire.Hosting.ApplicationModel;

// Put extensions in the Aspire.Hosting namespace to ease discovery as referencing
// the .NET Aspire hosting package automatically adds this namespace.
namespace Aspire.Hosting;

public static class MailDevResourceBuilderExtensions
{
    private const string UserEnvVarName = "MAILDEV_INCOMING_USER";
    private const string PasswordEnvVarName = "MAILDEV_INCOMING_PASS";

    /// <summary>
    /// Adds the <see cref="MailDevResource"/> to the given
    /// <paramref name="builder"/> instance. Uses the "2.1.0" tag.
    /// </summary>
    /// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
    /// <param name="name">The name of the resource.</param>
    /// <param name="httpPort">The HTTP port.</param>
    /// <param name="smtpPort">The SMTP port.</param>
    /// <returns>
    /// An <see cref="IResourceBuilder{MailDevResource}"/> instance that
    /// represents the added MailDev resource.
    /// </returns>
    public static IResourceBuilder<MailDevResource> AddMailDev(
        this IDistributedApplicationBuilder builder,
        string name,
        int? httpPort = null,
        int? smtpPort = null,
        IResourceBuilder<ParameterResource>? userName = null,
        IResourceBuilder<ParameterResource>? password = null)
    {
        var passwordParameter = password?.Resource ??
            ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(
                builder, $"{name}-password");

        // The AddResource method is a core API within .NET Aspire and is
        // used by resource developers to wrap a custom resource in an
        // IResourceBuilder<T> instance. Extension methods to customize
        // the resource (if any exist) target the builder interface.
        var resource = new MailDevResource(
            name, userName?.Resource, passwordParameter);

        return builder.AddResource(resource)
                      .WithImage(MailDevContainerImageTags.Image)
                      .WithImageRegistry(MailDevContainerImageTags.Registry)
                      .WithImageTag(MailDevContainerImageTags.Tag)
                      .WithHttpEndpoint(
                          targetPort: 1080,
                          port: httpPort,
                          name: MailDevResource.HttpEndpointName)
                      .WithEndpoint(
                          targetPort: 1025,
                          port: smtpPort,
                          name: MailDevResource.SmtpEndpointName)
                      .WithEnvironment(context =>
                      {
                          context.EnvironmentVariables[UserEnvVarName] = resource.UserNameReference;
                          context.EnvironmentVariables[PasswordEnvVarName] = resource.PasswordParameter;
                      });
    }
}

// This class just contains constant strings that can be updated periodically
// when new versions of the underlying container are released.
internal static class MailDevContainerImageTags
{
    internal const string Registry = "docker.io";

    internal const string Image = "maildev/maildev";

    internal const string Tag = "2.1.0";
}

Le code précédent met à jour la méthode d’extension AddMailDev pour inclure les paramètres userName et password. La méthode WithEnvironment est mise à jour pour inclure les variables d’environnement UserEnvVarName et PasswordEnvVarName. Ces variables d’environnement sont utilisées pour définir le nom d’utilisateur et le mot de passe MailDev.

Mettre à jour l’hôte de l’application

Maintenant que la ressource est mise à jour pour inclure les paramètres de nom d’utilisateur et de mot de passe, vous devez mettre à jour l’hôte de l’application pour inclure ces paramètres. Mettez à jour le fichier Program.cs dans le projet MailDevResource.AppHost avec le code C# suivant :

var builder = DistributedApplication.CreateBuilder(args);

var mailDevUsername = builder.AddParameter("maildev-username");
var mailDevPassword = builder.AddParameter("maildev-password");

var maildev = builder.AddMailDev(
    name: "maildev",
    userName: mailDevUsername,
    password: mailDevPassword);

builder.AddProject<Projects.MailDevResource_NewsletterService>("newsletterservice")
       .WithReference(maildev);

builder.Build().Run();

Le code précédent ajoute deux paramètres pour le nom d’utilisateur et le mot de passe MailDev. Il affecte ces paramètres aux variables d’environnement MAILDEV_INCOMING_USER et MAILDEV_INCOMING_PASS. La méthode AddMailDev a deux appels chaînés à WithEnvironment qui inclut ces variables d’environnement. Pour plus d’informations sur les paramètres, consultez paramètres externes.

Ensuite, configurez les secrets pour ces paramètres. Cliquez avec le bouton droit sur le projet MailDevResource.AppHost, puis sélectionnez Manage User Secrets. Ajoutez le JSON suivant aux secrets dans le fichierjson.

{
  "Parameters:maildev-username": "@admin",
  "Parameters:maildev-password": "t3st1ng"
}

Avertissement

Ces informations d’identification sont uniquement destinées à des fins de démonstration et MailDev est destinée au développement local. Ces informations d’identification sont fictives et ne doivent pas être utilisées dans un environnement de production.

Mettre à jour l’intégration de MailKit

Il est recommandé pour les intégrations client de s’attendre à ce que les chaînes de connexion contiennent différentes paires clé/valeur et de parser ces paires dans les propriétés appropriées. Mettez à jour le fichier MailKitClientSettings.cs dans le projet MailKit.Client avec le code C# suivant :

using System.Data.Common;
using System.Net;

namespace MailKit.Client;

/// <summary>
/// Provides the client configuration settings for connecting MailKit to an SMTP server.
/// </summary>
public sealed class MailKitClientSettings
{
    internal const string DefaultConfigSectionName = "MailKit:Client";

    /// <summary>
    /// Gets or sets the SMTP server <see cref="Uri"/>.
    /// </summary>
    /// <value>
    /// The default value is <see langword="null"/>.
    /// </value>
    public Uri? Endpoint { get; set; }

    /// <summary>
    /// Gets or sets the network credentials that are optionally configurable for SMTP
    /// server's that require authentication.
    /// </summary>
    /// <value>
    /// The default value is <see langword="null"/>.
    /// </value>
    public NetworkCredential? Credentials { get; set; }

    /// <summary>
    /// Gets or sets a boolean value that indicates whether the database health check is disabled or not.
    /// </summary>
    /// <value>
    /// The default value is <see langword="false"/>.
    /// </value>
    public bool DisableHealthChecks { get; set; }

    /// <summary>
    /// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not.
    /// </summary>
    /// <value>
    /// The default value is <see langword="false"/>.
    /// </value>
    public bool DisableTracing { get; set; }

    /// <summary>
    /// Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are disabled or not.
    /// </summary>
    /// <value>
    /// The default value is <see langword="false"/>.
    /// </value>
    public bool DisableMetrics { get; set; }

    internal void ParseConnectionString(string? connectionString)
    {
        if (string.IsNullOrWhiteSpace(connectionString))
        {
            throw new InvalidOperationException($"""
                    ConnectionString is missing.
                    It should be provided in 'ConnectionStrings:<connectionName>'
                    or '{DefaultConfigSectionName}:Endpoint' key.'
                    configuration section.
                    """);
        }

        if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
        {
            Endpoint = uri;
        }
        else
        {
            var builder = new DbConnectionStringBuilder
            {
                ConnectionString = connectionString
            };
            
            if (builder.TryGetValue("Endpoint", out var endpoint) is false)
            {
                throw new InvalidOperationException($"""
                        The 'ConnectionStrings:<connectionName>' (or 'Endpoint' key in
                        '{DefaultConfigSectionName}') is missing.
                        """);
            }

            if (Uri.TryCreate(endpoint.ToString(), UriKind.Absolute, out uri) is false)
            {
                throw new InvalidOperationException($"""
                        The 'ConnectionStrings:<connectionName>' (or 'Endpoint' key in
                        '{DefaultConfigSectionName}') isn't a valid URI.
                        """);
            }

            Endpoint = uri;
            
            if (builder.TryGetValue("Username", out var username) &&
                builder.TryGetValue("Password", out var password))
            {
                Credentials = new(
                    username.ToString(), password.ToString());
            }
        }
    }
}

La classe de paramètres précédente inclut désormais une propriété Credentials de type NetworkCredential. La méthode ParseConnectionString est mise à jour pour analyser les clés Username et Password à partir de la chaîne de connexion. Si les clés Username et Password sont présentes, une NetworkCredential est créée et affectée à la propriété Credentials.

Avec la classe de configuration mise à jour pour comprendre et remplir les informations d’identification, mettez à jour la fabrique pour utiliser les informations d’identification de manière conditionnelle si elles sont configurées. Mettez à jour le fichier MailKitClientFactory.cs dans le projet MailKit.Client avec le code C# suivant :

using System.Net;
using MailKit.Net.Smtp;

namespace MailKit.Client;

/// <summary>
/// A factory for creating <see cref="ISmtpClient"/> instances
/// given a <paramref name="smtpUri"/> (and optional <paramref name="credentials"/>).
/// </summary>
/// <param name="settings">
/// The <see cref="MailKitClientSettings"/> settings for the SMTP server
/// </param>
public sealed class MailKitClientFactory(MailKitClientSettings settings) : IDisposable
{
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    private SmtpClient? _client;

    /// <summary>
    /// Gets an <see cref="ISmtpClient"/> instance in the connected state
    /// (and that's been authenticated if configured).
    /// </summary>
    /// <param name="cancellationToken">Used to abort client creation and connection.</param>
    /// <returns>A connected (and authenticated) <see cref="ISmtpClient"/> instance.</returns>
    /// <remarks>
    /// Since both the connection and authentication are considered expensive operations,
    /// the <see cref="ISmtpClient"/> returned is intended to be used for the duration of a request
    /// (registered as 'Scoped') and is automatically disposed of.
    /// </remarks>
    public async Task<ISmtpClient> GetSmtpClientAsync(
        CancellationToken cancellationToken = default)
    {
        await _semaphore.WaitAsync(cancellationToken);

        try
        {
            if (_client is null)
            {
                _client = new SmtpClient();

                await _client.ConnectAsync(settings.Endpoint, cancellationToken)
                             .ConfigureAwait(false);

                if (settings.Credentials is not null)
                {
                    await _client.AuthenticateAsync(settings.Credentials, cancellationToken)
                                 .ConfigureAwait(false);
                }
            }
        }
        finally
        {
            _semaphore.Release();
        }       

        return _client;
    }

    public void Dispose()
    {
        _client?.Dispose();
        _semaphore.Dispose();
    }
}

Lorsque l'usine détermine que les identifiants ont été configurés, elle s’authentifie auprès du server SMTP après s'être connectée avant de retourner le SmtpClient.

Exécuter l’exemple

Maintenant que vous avez mis à jour la ressource, les projets d’intégration correspondants et l’hôte de l’application, vous êtes prêt à exécuter l’exemple d’application. Pour exécuter l’exemple à partir de votre IDE, sélectionnez F5 ou utilisez dotnet run à partir du répertoire racine de la solution pour démarrer l’application. Vous devez voir le tableau de bord .NET.NET Aspire. Accédez à la ressource de conteneur maildev et consultez les détails. Vous devez voir les paramètres de nom d’utilisateur et de mot de passe dans les détails de la ressource, sous la section Variables d’environnement :

.NET Aspire Tableau de bord : MailDev détails des ressources de conteneur.

De même, vous devez voir la chaîne de connexion dans les détails de la ressource , sous la section variables d’environnement :

.NET.NET Aspire Tableau de bord : Détails des ressources du service Bulletin d’informations.

Vérifiez que tout fonctionne comme prévu.

Résumé

Cet article a démontré comment faire circuler des informations d'identification d'authentification d'une ressource personnalisée vers une intégration client personnalisée. La ressource personnalisée est un conteneur MailDev qui autorise les informations d’identification entrantes ou sortantes. L’intégration personnalisée client est un MailKit client pour l'envoi d'e-mails. En mettant à jour la ressource pour inclure les paramètres username et password, et en mettant à jour l’intégration pour analyser et utiliser ces paramètres, l’authentification circule les informations d’identification de l’intégration d’hébergement vers l’intégration client.