File: SecurePluginCredentialProvider.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Credentials\NuGet.Credentials.csproj (NuGet.Credentials)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable disable

using System;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Configuration;
using NuGet.Protocol.Plugins;

namespace NuGet.Credentials
{
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
    public sealed class SecurePluginCredentialProvider : ICredentialProvider
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
    {
        private const string _basicAuthenticationType = "Basic";

        /// <summary>
        /// Plugin that this provider will use to acquire credentials
        /// </summary>
        private readonly PluginDiscoveryResult _discoveredPlugin;

        /// <summary>
        /// logger
        /// </summary>
        private readonly ILogger _logger;

        /// <summary>
        /// pluginManager
        /// </summary>
        private readonly IPluginManager _pluginManager;

        /// <summary>
        /// canShowDialog, whether the plugin can prompt or it should use device flow. This is a host decision not a user one. 
        /// </summary>
        private readonly bool _canShowDialog;

        // We use this to avoid needlessly instantiating plugins if they don't support authentication.
        private bool _isAnAuthenticationPlugin = true;

        /// <summary>
        /// Create a credential provider based on provided plugin
        /// </summary>
        /// <param name="pluginManager"></param>
        /// <param name="pluginDiscoveryResult"></param>
        /// <param name="canShowDialog"></param>
        /// <param name="logger"></param>
        /// <exception cref="ArgumentNullException">if <paramref name="pluginDiscoveryResult"/> is null</exception>
        /// <exception cref="ArgumentNullException">if <paramref name="logger"/> is null</exception>
        /// <exception cref="ArgumentNullException">if <paramref name="pluginManager"/> is null</exception>
        /// <exception cref="ArgumentException">if plugin file is not valid</exception>
        public SecurePluginCredentialProvider(IPluginManager pluginManager, PluginDiscoveryResult pluginDiscoveryResult, bool canShowDialog, ILogger logger)
        {
            _pluginManager = pluginManager ?? throw new ArgumentNullException(nameof(pluginManager));
            _discoveredPlugin = pluginDiscoveryResult ?? throw new ArgumentNullException(nameof(pluginDiscoveryResult));
            _canShowDialog = canShowDialog;
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            Id = $"{nameof(SecurePluginCredentialProvider)}_{pluginDiscoveryResult.PluginFile.Path}";
        }

        /// <summary>
        /// Unique identifier of this credential provider
        /// </summary>
        public string Id { get; }

        /// <param name="uri">The uri of a web resource for which credentials are needed.</param>
        /// <param name="proxy">Ignored.  Proxy information will not be passed to plugins.</param>
        /// <param name="type">
        /// The type of credential request that is being made. Note that this implementation of
        /// <see cref="ICredentialProvider"/> does not support providing proxy credenitials and treats
        /// all other types the same.
        /// </param>
        /// <param name="isRetry">If true, credentials were previously supplied by this
        /// provider for the same uri.</param>
        /// <param name="message">A message provided by NuGet to show to the user when prompting.</param>
        /// <param name="nonInteractive">If true, the plugin must not prompt for credentials.</param>
        /// <param name="cancellationToken">A cancellation token.</param>
        /// <returns>A credential object.</returns>
        public async Task<CredentialResponse> GetAsync(Uri uri, IWebProxy proxy, CredentialRequestType type, string message, bool isRetry, bool nonInteractive, CancellationToken cancellationToken)
        {
            CredentialResponse taskResponse = null;
            if (type == CredentialRequestType.Proxy || !_isAnAuthenticationPlugin)
            {
                taskResponse = new CredentialResponse(CredentialStatus.ProviderNotApplicable);
                return taskResponse;
            }

            Tuple<bool, PluginCreationResult> result = await _pluginManager.TryGetSourceAgnosticPluginAsync(_discoveredPlugin, OperationClaim.Authentication, cancellationToken);

            bool wasSomethingCreated = result.Item1;

            if (wasSomethingCreated)
            {
                PluginCreationResult creationResult = result.Item2;

                if (!string.IsNullOrEmpty(creationResult.Message))
                {
                    // There is a potential here for double logging as the CredentialService itself catches the exceptions and tries to log it.
                    // In reality the logger in the Credential Service will be null because the first request always comes from a resource provider (ServiceIndex provider).
                    _logger.LogError(creationResult.Message);

                    if (creationResult.Exception != null)
                    {
                        _logger.LogDebug(creationResult.Exception.ToString());
                    }
                    _isAnAuthenticationPlugin = false;
                    throw new PluginException(creationResult.Message, creationResult.Exception); // Throwing here will block authentication and ensure that the complete operation fails.
                }

                _isAnAuthenticationPlugin = creationResult.Claims.Contains(OperationClaim.Authentication);

                if (_isAnAuthenticationPlugin)
                {
                    AddOrUpdateLogger(creationResult.Plugin);
                    await SetPluginLogLevelAsync(creationResult, _logger, cancellationToken);

                    if (proxy != null)
                    {
                        await SetProxyCredentialsToPlugin(uri, proxy, creationResult, cancellationToken);
                    }

                    var request = new GetAuthenticationCredentialsRequest(uri, isRetry, nonInteractive, _canShowDialog);
                    var credentialResponse = await creationResult.Plugin.Connection.SendRequestAndReceiveResponseAsync<GetAuthenticationCredentialsRequest, GetAuthenticationCredentialsResponse>(
                        MessageMethod.GetAuthenticationCredentials,
                        request,
                        cancellationToken);
                    if (credentialResponse.ResponseCode == MessageResponseCode.NotFound && nonInteractive)
                    {
                        _logger.LogWarning(
                            string.Format(
                                CultureInfo.CurrentCulture,
                                Resources.SecurePluginWarning_UseInteractiveOption));
                    }

                    taskResponse = GetAuthenticationCredentialsResponseToCredentialResponse(credentialResponse);
                }
            }
            else
            {
                _isAnAuthenticationPlugin = false;
            }

            return taskResponse ?? new CredentialResponse(CredentialStatus.ProviderNotApplicable);
        }

        private async Task SetPluginLogLevelAsync(PluginCreationResult plugin, ILogger logger, CancellationToken cancellationToken)
        {
            var logLevel = LogRequestHandler.GetLogLevel(logger);

            await plugin.PluginMulticlientUtilities.DoOncePerPluginLifetimeAsync(
                MessageMethod.SetLogLevel.ToString(),
                () => plugin.Plugin.Connection.SendRequestAndReceiveResponseAsync<SetLogLevelRequest, SetLogLevelResponse>(
                    MessageMethod.SetLogLevel,
                    new SetLogLevelRequest(logLevel),
                    cancellationToken),
                cancellationToken);
        }

        private void AddOrUpdateLogger(IPlugin plugin)
        {
            plugin.Connection.MessageDispatcher.RequestHandlers.AddOrUpdate(
                MessageMethod.Log,
                () => new LogRequestHandler(_logger),
                existingHandler =>
                {
                    ((LogRequestHandler)existingHandler).SetLogger(_logger);

                    return existingHandler;
                });
        }

        private async Task SetProxyCredentialsToPlugin(Uri uri, IWebProxy proxy, PluginCreationResult plugin, CancellationToken cancellationToken)
        {
            var proxyCredential = proxy.Credentials.GetCredential(uri, _basicAuthenticationType);

            var key = $"{MessageMethod.SetCredentials}.{Id}";

            var proxyCredRequest = new SetCredentialsRequest(
                uri.AbsolutePath,
                proxyCredential?.UserName,
                proxyCredential?.Password,
                username: null,
                password: null);

            await plugin.PluginMulticlientUtilities.DoOncePerPluginLifetimeAsync(
                 key,
                 () =>
                     plugin.Plugin.Connection.SendRequestAndReceiveResponseAsync<SetCredentialsRequest, SetCredentialsResponse>(
                     MessageMethod.SetCredentials,
                     proxyCredRequest,
                     cancellationToken),
                 cancellationToken);
        }

        /// <summary>
        /// Convert from Plugin CredentialResponse to the CredentialResponse model used by the ICredentialService
        /// </summary>
        /// <param name="credentialResponse"></param>
        /// <returns>credential response</returns>
        private static CredentialResponse GetAuthenticationCredentialsResponseToCredentialResponse(GetAuthenticationCredentialsResponse credentialResponse)
        {
            CredentialResponse taskResponse;
            if (credentialResponse.IsValid())
            {
                ICredentials result = new AuthTypeFilteredCredentials(
                    new NetworkCredential(credentialResponse.Username, credentialResponse.Password),
                    credentialResponse.AuthenticationTypes ?? Enumerable.Empty<string>());

                taskResponse = new CredentialResponse(result);
            }
            else if (credentialResponse.ResponseCode == MessageResponseCode.NotFound)
            {
                taskResponse = new CredentialResponse(CredentialStatus.UserCanceled);
            }
            else
            {
                taskResponse = new CredentialResponse(CredentialStatus.ProviderNotApplicable);
            }

            return taskResponse;
        }
    }
}