VssConnection到VSTS始终会提示输入凭据

时间:2017-11-10 20:55:26

标签: c# visual-studio caching azure-devops azure-devops-rest-api

我正在使用Visual Studio客户端工具在命令行实用程序中调用VSTS REST API。对于不同的命令(复制,删除,应用策略等),可以多次运行此实用程序

我正在创建像这样的

的VssConnection
public static VssConnection CreateConnection(Uri url, VssCredentials credentials = null)
{
    credentials = credentials ?? new VssClientCredentials();            

    credentials.Storage = new VssClientCredentialStorage();           

    var connection = new VssConnection(url, credentials);

    connection.ConnectAsync().SyncResult(); 

    return connection;
}

根据文档,这应该缓存凭据,以便在运行命令行工具时不会再次提示您。但每次运行命令行实用程序并且VssConnection尝试连接时,都会收到提示。

是否有缓存凭据的权限,以便每次运行命令行时都不会提示用户?

应该注意的是,如果我不处理VssConnection,下次运行它时就不会提示。

更新 需要明确的是,一旦创建连接,该问题就不会缓存VssClientCredentials实例,因为该对象已附加到VssConnection对象。问题是在程序执行之间,即在本地机器上缓存用户令牌,以便下次从命令行执行该实用程序时,用户不必再次键入其凭证。类似于每次启动时都不必总是登录到Visual Studio。

1 个答案:

答案 0 :(得分:3)

所以我找到了一个似乎正是我想要的工作解决方案。如果有更好的解决方案,请随时发布。

解决方案:由于VssClientCredentials.Storage属性需要一个实现IVssCredentialStorage的类,因此我创建了一个通过从库存VssClientCredentialStorage类派生来实现该接口的类。

然后它会覆盖检索和删除令牌的方法,以便根据与令牌在注册表中存储的过期租约来管理它们。

如果检索到令牌并且租约已过期,则会从存储中删除令牌并返回null,并且VssConnection类会显示一个UI,强制用户输入其凭据。如果令牌未过期,则不会提示用户并使用缓存的凭据。

所以现在我可以做到以下几点:

  • 首次从命令行调用我的实用程序
  • 为VSTS客户端提示提供凭据
  • 在没有提示的情况下从命令行再次运行该实用程序!

现在,我已在我的实用程序中内置了标准租约到期,但用户可以使用命令行选项对其进行更改。此外,用户也可以清除缓存的凭据。

密钥位于RemoveToken覆盖中。对基类的调用是从注册表中删除它的,所以如果你绕过它(在我的情况下如果租约没有过期)那么注册表项将保留。这允许VssConnection使用缓存的凭据,而不是每次执行程序时都提示用户!

主叫代码示例:

    public static VssConnection CreateConnection(Uri url, VssCredentials credentials = null, double tokenLeaseInSeconds = VssClientCredentialCachingStorage.DefaultTokenLeaseInSeconds)
    {
        credentials = credentials ?? new VssClientCredentials();

        credentials.Storage = GetVssClientCredentialStorage(tokenLeaseInSeconds);

        var connection = new VssConnection(url, credentials);

        connection.ConnectAsync().SyncResult(); 

        return connection;
    }

    private static VssClientCredentialCachingStorage GetVssClientCredentialStorage(double tokenLeaseInSeconds)
    {
        return new VssClientCredentialCachingStorage("YourApp", "YourNamespace", tokenLeaseInSeconds);
    }

派生的存储类:

    /// <summary>
    /// Class to alter the credential storage behavior to allow the token to be cached between sessions.
    /// </summary>
    /// <seealso cref="Microsoft.VisualStudio.Services.Common.IVssCredentialStorage" />
    public class VssClientCredentialCachingStorage : VssClientCredentialStorage
    {
        #region [Private]

        private const string __tokenExpirationKey = "ExpirationDateTime";
        private double _tokenLeaseInSeconds;

        #endregion [Private]

        /// <summary>
        /// The default token lease in seconds
        /// </summary>
        public const double DefaultTokenLeaseInSeconds = 86400;// one day

        /// <summary>
        /// Initializes a new instance of the <see cref="VssClientCredentialCachingStorage"/> class.
        /// </summary>
        /// <param name="storageKind">Kind of the storage.</param>
        /// <param name="storageNamespace">The storage namespace.</param>
        /// <param name="tokenLeaseInSeconds">The token lease in seconds.</param>
        public VssClientCredentialCachingStorage(string storageKind = "VssApp", string storageNamespace = "VisualStudio", double tokenLeaseInSeconds = DefaultTokenLeaseInSeconds)
            : base(storageKind, storageNamespace)
        {
            this._tokenLeaseInSeconds = tokenLeaseInSeconds;
        }

        /// <summary>
        /// Removes the token.
        /// </summary>
        /// <param name="serverUrl">The server URL.</param>
        /// <param name="token">The token.</param>
        public override void RemoveToken(Uri serverUrl, IssuedToken token)
        {
            this.RemoveToken(serverUrl, token, false);
        }

        /// <summary>
        /// Removes the token.
        /// </summary>
        /// <param name="serverUrl">The server URL.</param>
        /// <param name="token">The token.</param>
        /// <param name="force">if set to <c>true</c> force the removal of the token.</param>
        public void RemoveToken(Uri serverUrl, IssuedToken token, bool force)
        {
            //////////////////////////////////////////////////////////
            // Bypassing this allows the token to be stored in local
            // cache. Token is removed if lease is expired.

            if (force || token != null && this.IsTokenExpired(token))
                base.RemoveToken(serverUrl, token);

            //////////////////////////////////////////////////////////
        }

        /// <summary>
        /// Retrieves the token.
        /// </summary>
        /// <param name="serverUrl">The server URL.</param>
        /// <param name="credentialsType">Type of the credentials.</param>
        /// <returns>The <see cref="IssuedToken"/></returns>
        public override IssuedToken RetrieveToken(Uri serverUrl, VssCredentialsType credentialsType)
        {
            var token = base.RetrieveToken(serverUrl, credentialsType);            

            if (token != null)
            {
                bool expireToken = this.IsTokenExpired(token);
                if (expireToken)
                {
                    base.RemoveToken(serverUrl, token);
                    token = null;
                }
                else
                {
                    // if retrieving the token before it is expired,
                    // refresh the lease period.
                    this.RefreshLeaseAndStoreToken(serverUrl, token);
                    token = base.RetrieveToken(serverUrl, credentialsType);
                }
            }

            return token;
        }

        /// <summary>
        /// Stores the token.
        /// </summary>
        /// <param name="serverUrl">The server URL.</param>
        /// <param name="token">The token.</param>
        public override void StoreToken(Uri serverUrl, IssuedToken token)
        {
            this.RefreshLeaseAndStoreToken(serverUrl, token);
        }

        /// <summary>
        /// Clears all tokens.
        /// </summary>
        /// <param name="url">The URL.</param>
        public void ClearAllTokens(Uri url = null)
        {
            IEnumerable<VssToken> tokens = this.TokenStorage.RetrieveAll(base.TokenKind).ToList();

            if (url != default(Uri))
                tokens = tokens.Where(t => StringComparer.InvariantCultureIgnoreCase.Compare(t.Resource, url.ToString().TrimEnd('/')) == 0);

            foreach(var token in tokens)
                this.TokenStorage.Remove(token);
        }

        private void RefreshLeaseAndStoreToken(Uri serverUrl, IssuedToken token)
        {
            if (token.Properties == null)
                token.Properties = new Dictionary<string, string>();

            token.Properties[__tokenExpirationKey] = JsonSerializer.SerializeObject(this.GetNewExpirationDateTime());

            base.StoreToken(serverUrl, token);
        }

        private DateTime GetNewExpirationDateTime()
        {
            var now = DateTime.Now;

            // Ensure we don't overflow the max DateTime value
            var lease = Math.Min((DateTime.MaxValue - now.Add(TimeSpan.FromSeconds(1))).TotalSeconds, this._tokenLeaseInSeconds);

            // ensure we don't have negative leases
            lease = Math.Max(lease, 0);

            return now.AddSeconds(lease);            
        }

        private bool IsTokenExpired(IssuedToken token)
        {
            bool expireToken = true;

            if (token != null && token.Properties.ContainsKey(__tokenExpirationKey))
            {
                string expirationDateTimeJson = token.Properties[__tokenExpirationKey];

                try
                {
                    DateTime expiration = JsonSerializer.DeserializeObject<DateTime>(expirationDateTimeJson);

                    expireToken = DateTime.Compare(DateTime.Now, expiration) >= 0;
                }
                catch { }
            }

            return expireToken;
        }
    }