| File: SecurePluginCredentialProvider.cs | Web Access |
| Project: 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; } } }