File: PackageSource\PackageSourceCredential.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Configuration\NuGet.Configuration.csproj (NuGet.Configuration)
// 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.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using NuGet.Common;
using NuGet.Shared;

namespace NuGet.Configuration
{
    /// <summary>
    /// Represents credentials required to authenticate user within package source web requests.
    /// </summary>
    public class PackageSourceCredential : IEquatable<PackageSourceCredential>
    {
        /// <summary>
        /// User name
        /// </summary>
        public string Username { get; }

        /// <summary>
        /// Password text as stored in config file. May be encrypted.
        /// </summary>
        public string PasswordText { get; }

        /// <summary>
        /// Indicates if password is stored in clear text.
        /// </summary>
        public bool IsPasswordClearText { get; }

        /// <summary>
        /// List of authentication types the credential is valid for, e.g. 'basic'. If empty, all authentication types
        /// are allowed.
        /// </summary>
        public IEnumerable<string> ValidAuthenticationTypes =>
            ParseAuthTypeFilterString(ValidAuthenticationTypesText);

        private readonly Lazy<int> _hashCode;

        /// <summary>
        /// Comma-delimited list of authentication types the credential is valid for as stored in the config file.
        /// If null or empty, all authentication types are valid. Example: 'basic,negotiate'
        /// </summary>
        public string? ValidAuthenticationTypesText { get; }

        /// <summary>
        /// Retrieves password in clear text. Decrypts on-demand.
        /// </summary>
        public string Password
        {
            get
            {
                if (PasswordText != null && !IsPasswordClearText)
                {
                    try
                    {
                        return EncryptionUtility.DecryptString(PasswordText);
                    }
                    catch (NotSupportedException e)
                    {
                        throw new NuGetConfigurationException(
                            string.Format(CultureInfo.CurrentCulture, Resources.UnsupportedDecryptPassword, Source), e);
                    }
                }
                else
                {
                    return PasswordText!;
                }
            }
        }

        /// <summary>
        /// Associated source ID
        /// </summary>
        public string Source { get; }

        /// <summary>
        /// Verifies if object contains valid data, e.g. not empty user name and password.
        /// </summary>
        /// <returns>True if credentials object is valid</returns>
        public bool IsValid() => !string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(PasswordText);

        /// <summary>
        /// Instantiates the credential instance out of raw values read from a config file.
        /// </summary>
        /// <param name="source">Associated source ID (needed for reporting errors)</param>
        /// <param name="username">User name</param>
        /// <param name="passwordText">Password as stored in config file</param>
        /// <param name="isPasswordClearText">Hints if password provided in clear text</param>
        /// <param name="validAuthenticationTypesText">
        /// Comma-delimited list of authentication types the credential is valid for as stored in the config file.
        /// If null or empty, all authentication types are valid. Example: 'basic,negotiate'
        /// </param>
        public PackageSourceCredential(string source, string username, string passwordText, bool isPasswordClearText, string? validAuthenticationTypesText)
        {
            Source = source ?? throw new ArgumentNullException(nameof(source));
            Username = username ?? throw new ArgumentNullException(nameof(username));
            PasswordText = passwordText;
            IsPasswordClearText = isPasswordClearText;

            // validAuthenticationTypesText is permitted to be null
            ValidAuthenticationTypesText = validAuthenticationTypesText;

            _hashCode = new Lazy<int>(() =>
            {
                var combiner = new HashCodeCombiner();

                combiner.AddObject(Source);
                combiner.AddObject(Username);
                combiner.AddObject(PasswordText);
                combiner.AddObject(IsPasswordClearText);

                return combiner.CombinedHash;
            });
        }

        /// <summary>
        /// Creates new instance of credential object out values provided by user.
        /// </summary>
        /// <param name="source">Source ID needed for reporting errors if any</param>
        /// <param name="username">User name</param>
        /// <param name="password">Password text in clear</param>
        /// <param name="storePasswordInClearText">Hints if the password should be stored in clear text on disk.</param>
        /// <param name="validAuthenticationTypesText">
        /// Comma-delimited list of authentication types the credential is valid for as stored in the config file.
        /// If null or empty, all authentication types are valid. Example: 'basic,negotiate'
        /// </param>
        /// <returns>New instance of <see cref="PackageSourceCredential"/></returns>
        public static PackageSourceCredential FromUserInput(
            string source,
            string username,
            string password,
            bool storePasswordInClearText,
            string? validAuthenticationTypesText)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            if (username == null)
            {
                throw new ArgumentNullException(nameof(username));
            }

            if (password == null)
            {
                throw new ArgumentNullException(nameof(password));
            }

            // validAuthenticationTypes is permitted to be null.

            try
            {
                var passwordText = storePasswordInClearText ? password : EncryptionUtility.EncryptString(password);
                return new PackageSourceCredential(
                    source,
                    username,
                    passwordText,
                    isPasswordClearText: storePasswordInClearText,
                    validAuthenticationTypesText: validAuthenticationTypesText);
            }
            catch (NotSupportedException e)
            {
                throw new NuGetConfigurationException(
                    string.Format(CultureInfo.CurrentCulture, Resources.UnsupportedEncryptPassword, source), e);
            }
        }

        /// <summary>
        /// Converts this object to an ICredentials, capturing the username, password and valid authentication types
        /// </summary>
        public ICredentials ToICredentials()
        {
            return new AuthTypeFilteredCredentials(new NetworkCredential(Username, Password), ValidAuthenticationTypes);
        }

        internal PackageSourceCredential Clone()
        {
            return new PackageSourceCredential(Source, Username, PasswordText, IsPasswordClearText, ValidAuthenticationTypesText);
        }

        /// <summary>
        /// Converts an authentication type filter string from the config file syntax to a list of valid authentication
        /// types
        /// </summary>
        /// <param name="str">
        /// Comma-delimited list of authentication types the credential is valid for as stored in the config file.
        /// If null or empty, all authentication types are valid. Example: 'basic,negotiate'
        /// </param>
        /// <returns>
        /// Enumeration of valid authentication types. If empty, all authentication types are valid.
        /// </returns>
        private static IEnumerable<string> ParseAuthTypeFilterString(string? str)
        {
            return string.IsNullOrWhiteSpace(str)
                ? Enumerable.Empty<string>()
                : str!.Split(',').Select(x => x.Trim()).Where(x => x != string.Empty);
        }


        public CredentialsItem AsCredentialsItem()
        {
            return new CredentialsItem(Source, Username, PasswordText, IsPasswordClearText, ValidAuthenticationTypesText);
        }

        public bool Equals(PackageSourceCredential? other)
        {
            if (other == null)
            {
                return false;
            }

            if (ReferenceEquals(this, other))
            {
                return true;
            }

            return string.Equals(Source, other.Source, StringComparison.Ordinal) &&
                string.Equals(Username, other.Username, StringComparison.Ordinal) &&
                string.Equals(PasswordText, other.PasswordText, StringComparison.Ordinal) &&
                IsPasswordClearText == other.IsPasswordClearText;
        }

        public override bool Equals(object? other)
        {
            return Equals(other as PackageSourceCredential);
        }

        public override int GetHashCode()
        {
            return _hashCode.Value;
        }
    }
}