/// <summary> /// Validates that a `<see cref="Token"/>` is valid to grant access to the VSTS resource referenced by `<paramref name="targetUri"/>`. /// <para/> /// Returns `<see langword="true"/>` if successful; otherwise `<see langword="false"/>`. /// </summary> /// <param name="targetUri">URI of the VSTS resource.</param> /// <param name="token">`<see cref="Token"/>` expected to grant access to the VSTS resource.</param> public async Task <bool> ValidateToken(TargetUri targetUri, Token token) { BaseSecureStore.ValidateTargetUri(targetUri); BaseSecureStore.ValidateToken(token); // Personal access tokens are effectively credentials, treat them as such. if (token.Type == TokenType.Personal) { return(await ValidateCredentials(targetUri, (Credential)token)); } try { // Create an request to the VSTS deployment data end-point. HttpWebRequest request = GetConnectionDataRequest(targetUri, token); Git.Trace.WriteLine($"validating token against '{request.Host}'."); // Send the request and wait for the response. using (HttpWebResponse response = await request.GetResponseAsync() as HttpWebResponse) { // We're looking for 'OK 200' here, anything else is failure. Git.Trace.WriteLine($"server returned: '{response.StatusCode}'."); return(response.StatusCode == HttpStatusCode.OK); } } catch (WebException webException) { Git.Trace.WriteLine($"! server returned: '{webException.Message}'."); } catch { Git.Trace.WriteLine("! unexpected error"); } Git.Trace.WriteLine($"token validation for '{targetUri}' failed."); return(false); }
/// <summary> /// Acquires credentials via the registered callbacks. /// </summary> /// <param name="targetUri"> /// The uniform resource indicator used to uniquely identify the credentials. /// </param> /// <returns>If successful a <see cref="Credential"/> object from the authentication object, /// authority or storage; otherwise <see langword="null"/>.</returns> public async Task <Credential> AcquireCredentials(TargetUri targetUri) { BaseSecureStore.ValidateTargetUri(targetUri); // get the WWW-Authenticate headers (if any) if (_httpAuthenticateOptions == null) { _httpAuthenticateOptions = await WwwAuthenticateHelper.GetHeaderValues(targetUri); } // if the headers contain NTML as an option, then fall back to NTLM if (_httpAuthenticateOptions.Any(x => WwwAuthenticateHelper.IsNtlm(x))) { Git.Trace.WriteLine($"'{targetUri}' supports NTLM, sending NTLM credentials instead"); return(NtlmCredentials); } Credential credentials = null; if (_acquireCredentials != null) { Git.Trace.WriteLine($"prompting user for credentials for '{targetUri}'."); credentials = _acquireCredentials(targetUri); if (_acquireResult != null) { AcquireCredentialResult result = (credentials == null) ? AcquireCredentialResult.Failed : AcquireCredentialResult.Suceeded; _acquireResult(targetUri, result); } } return(credentials); }
/// <summary> /// Gets a token from the cache. /// </summary> /// <param name="targetUri">The key which to find the token.</param> /// <returns>A <see cref="Token"/> if successful; otherwise <see langword="null"/>.</returns> public Token ReadToken(TargetUri targetUri) { BaseSecureStore.ValidateTargetUri(targetUri); Trace.WriteLine("SecretCache::ReadToken"); Token token = null; string targetName = this.GetTargetName(targetUri); lock (_cache) { if (_cache.ContainsKey(targetName) && _cache[targetName] is Token) { token = _cache[targetName] as Token; } else { token = null; } } return(token); }
/// <summary> /// Reads credentials for a target URI from the credential store /// </summary> /// <param name="targetUri">The URI of the target for which credentials are being read</param> /// <returns>A <see cref="Credential"/> from the store; <see langword="null"/> if failure.</returns> public Credential ReadCredentials(TargetUri targetUri) { BaseSecureStore.ValidateTargetUri(targetUri); Trace.WriteLine("SecretCache::ReadCredentials"); Credential credentials = null; string targetName = this.GetTargetName(targetUri); lock (_cache) { if (_cache.ContainsKey(targetName) && _cache[targetName] is Credential) { credentials = _cache[targetName] as Credential; } else { credentials = null; } } return(credentials); }
/// <summary> /// Creates a new authentication broker based for the specified resource. /// </summary> /// <param name="targetUri">The resource for which authentication is being requested.</param> /// <param name="scope">The scope of the access being requested.</param> /// <param name="personalAccessTokenStore">Storage container for personal access token secrets.</param> /// <param name="adaRefreshTokenStore">Storage container for Azure access token secrets.</param> /// <param name="authentication"> /// An implementation of <see cref="BaseAuthentication"/> if one was detected; /// <see langword="null"/> otherwise. /// </param> /// <returns> /// <see langword="true"/> if an authority could be determined; <see langword="false"/> otherwise. /// </returns> public static BaseAuthentication GetAuthentication( TargetUri targetUri, VstsTokenScope scope, ICredentialStore personalAccessTokenStore) { BaseSecureStore.ValidateTargetUri(targetUri); if (ReferenceEquals(scope, null)) { throw new ArgumentNullException(nameof(scope)); } if (ReferenceEquals(personalAccessTokenStore, null)) { throw new ArgumentNullException(nameof(personalAccessTokenStore)); } BaseAuthentication authentication = null; Guid tenantId; if (DetectAuthority(targetUri, out tenantId)) { // empty Guid is MSA, anything else is AAD if (tenantId == Guid.Empty) { Git.Trace.WriteLine("MSA authority detected."); authentication = new VstsMsaAuthentication(scope, personalAccessTokenStore); } else { Git.Trace.WriteLine($"AAD authority for tenant '{tenantId}' detected."); authentication = new VstsAadAuthentication(tenantId, scope, personalAccessTokenStore); (authentication as VstsAadAuthentication).TenantId = tenantId; } } return(authentication); }
/// <summary> /// Generates a "personal access token" or service specific, usage restricted access token. /// <para/> /// Returns `<see langword="true"/>` if successful; `<see langword="false"/>` otherwise. /// </summary> /// <param name="targetUri">The target resource for which to acquire the personal access token for.</param> /// <param name="accessToken">Azure Directory access token with privileges to grant access to the target resource.</param> /// <param name="requestCompactToken">Generates a compact token if `<see langword="true"/>`; generates a self describing token if `<see langword="false"/>`.</param> protected async Task <Credential> GeneratePersonalAccessToken( TargetUri targetUri, Token accessToken, bool requestCompactToken) { BaseSecureStore.ValidateTargetUri(targetUri); if (accessToken is null) { throw new ArgumentNullException(nameof(accessToken)); } Credential credential = null; Token personalAccessToken; if ((personalAccessToken = await VstsAuthority.GeneratePersonalAccessToken(targetUri, accessToken, TokenScope, requestCompactToken)) != null) { credential = (Credential)personalAccessToken; Trace.WriteLine($"personal access token created for '{targetUri}'."); try { await PersonalAccessTokenStore.WriteCredentials(targetUri, credential); } catch (Exception exception) { System.Diagnostics.Debug.WriteLine(exception); Trace.WriteLine($"failed to write credentials to the secure store."); Trace.WriteException(exception); } } return(credential); }
/// <summary> /// Opens an interactive logon prompt to acquire acquire an authentication token from the /// Microsoft Live authentication and identity service. /// </summary> /// <param name="targetUri"> /// The uniform resource indicator of the resource access tokens are being requested for. /// </param> /// <param name="requireCompactToken"> /// True if a compact access token is required; false if a standard token is acceptable. /// </param> /// <returns>A <see cref="Credential"/> for packing into a basic authentication header; /// otherwise <see langword="null"/>.</returns> public async Task <Credential> InteractiveLogon(TargetUri targetUri, bool requireCompactToken) { const string QueryParameters = "domain_hint=live.com&display=popup&site_id=501454&nux=1"; BaseSecureStore.ValidateTargetUri(targetUri); try { Token token; if ((token = await this.VstsAuthority.InteractiveAcquireToken(targetUri, this.ClientId, this.Resource, new Uri(RedirectUrl), QueryParameters)) != null) { Git.Trace.WriteLine($"token '{targetUri}' successfully acquired."); return(await this.GeneratePersonalAccessToken(targetUri, token, requireCompactToken)); } } catch (AdalException exception) { Debug.Write(exception); } Git.Trace.WriteLine($"failed to acquire token for '{targetUri}'."); return(null); }
/// <summary> /// <para>Creates an interactive logon session, using ADAL secure browser GUI, which /// enables users to authenticate with the Azure tenant and acquire the necessary access /// tokens to exchange for a VSTS personal access token.</para> /// <para>Tokens acquired are stored in the secure secret stores provided during /// initialization.</para> /// </summary> /// <param name="targetUri">The unique identifier for the resource for which access is to /// be acquired.</param> /// <param name="requestCompactToken"> /// <para>Requests a compact format personal access token; otherwise requests a standard /// personal access token.</para> /// <para>Compact tokens are necessary for clients which have restrictions on the size of /// the basic authentication header which they can create (example: Git).</para> /// </param> /// <returns>A <see cref="Credential"/> for packing into a basic authentication header; /// otherwise <see langword="null"/>.</returns> public async Task <Credential> InteractiveLogon(TargetUri targetUri, bool requestCompactToken) { BaseSecureStore.ValidateTargetUri(targetUri); Trace.WriteLine("VstsAadAuthentication::InteractiveLogon"); try { Token token; if ((token = await this.VstsAuthority.InteractiveAcquireToken(targetUri, this.ClientId, this.Resource, new Uri(RedirectUrl), null)) != null) { Trace.WriteLine(" token acquisition succeeded."); return(await this.GeneratePersonalAccessToken(targetUri, token, requestCompactToken)); } } catch (AdalException) { Trace.WriteLine(" token acquisition failed."); } Trace.WriteLine(" interactive logon failed"); return(null); }
/// <summary> /// Gets `<see cref="Credential"/>` from the storage used by the authentication object. /// <para/> /// Returns a `<see cref="Credential"/>` if successful; otherwise `<see langword="null"/>`. /// </summary> /// <param name="targetUri">The uniform resource indicator used to uniquely identify the credentials.</param> public override Credential GetCredentials(TargetUri targetUri) { BaseSecureStore.ValidateTargetUri(targetUri); return(_credentialStore.ReadCredentials(targetUri)); }
public static async Task <KeyValuePair <bool, Guid> > DetectAuthority(TargetUri targetUri) { const string VstsBaseUrlHost = "visualstudio.com"; const string VstsResourceTenantHeader = "X-VSS-ResourceTenant"; BaseSecureStore.ValidateTargetUri(targetUri); var tenantId = Guid.Empty; if (targetUri.Host.EndsWith(VstsBaseUrlHost, StringComparison.OrdinalIgnoreCase)) { Git.Trace.WriteLine($"'{targetUri}' is subdomain of '{VstsBaseUrlHost}', checking AAD vs MSA."); string tenant = null; WebResponse response; if (StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, "http") || StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, "https")) { // Query the cache first string tenantUrl = targetUri.ToString(); // Read the cache from disk var cache = await DeserializeTenantCache(); // Check the cache for an existing value if (cache.TryGetValue(tenantUrl, out tenantId)) { return(new KeyValuePair <bool, Guid>(true, tenantId)); } try { // Build a request that we expect to fail, do not allow redirect to sign in url var request = WebRequest.CreateHttp(targetUri); request.UserAgent = Global.UserAgent; request.Method = "HEAD"; request.AllowAutoRedirect = false; // Get the response from the server response = await request.GetResponseAsync(); } catch (WebException exception) { Git.Trace.WriteLine($"unable to get response from '{targetUri}' due to '{exception.Status}'."); // Given the number proxy related failures we see, emit a message about the failure potentially // being related to a proxy or gateway misconfiguration. switch (exception.Status) { case WebExceptionStatus.ConnectFailure: case WebExceptionStatus.PipelineFailure: case WebExceptionStatus.ProtocolError: case WebExceptionStatus.ReceiveFailure: case WebExceptionStatus.SecureChannelFailure: case WebExceptionStatus.SendFailure: case WebExceptionStatus.ServerProtocolViolation: case WebExceptionStatus.TrustFailure: { Git.Trace.WriteLine($"is there a proxy or gateway incorrectly configured?"); } break; } response = exception.Response; } // If the response exists and we have headers, parse them if (response != null && response.SupportsHeaders) { // Find the VSTS resource tenant entry tenant = response.Headers[VstsResourceTenantHeader]; if (!string.IsNullOrWhiteSpace(tenant) && Guid.TryParse(tenant, out tenantId)) { // Update the cache. cache[tenantUrl] = tenantId; // Write the cache to disk. await SerializeTenantCache(cache); // Success, notify the caller return(new KeyValuePair <bool, Guid>(true, tenantId)); } } } else { Git.Trace.WriteLine($"detected non-https based protocol: {targetUri.Scheme}."); } } // if all else fails, fallback to basic authentication return(new KeyValuePair <bool, Guid>(false, tenantId)); }
/// <summary> /// Formats a TargetName string based on the TargetUri base on the format started by git-credential-winstore /// </summary> /// <param name="targetUri">Uri of the target</param> /// <returns>Properly formatted TargetName string</returns> private string GetTargetName(TargetUri targetUri) { BaseSecureStore.ValidateTargetUri(targetUri); return(_getTargetName(targetUri, _namespace)); }
/// <summary> /// Deletes `<see cref="Credential"/>` from the storage used by the authentication object. /// </summary> /// <param name="targetUri">The uniform resource indicator used to uniquely identify the credentials.</param> public override void DeleteCredentials(TargetUri targetUri) { BaseSecureStore.ValidateTargetUri(targetUri); _credentialStore.DeleteCredentials(targetUri); }
public static async Task <KeyValuePair <bool, Guid> > DetectAuthority(TargetUri targetUri) { const string VstsBaseUrlHost = "visualstudio.com"; const string VstsResourceTenantHeader = "X-VSS-ResourceTenant"; BaseSecureStore.ValidateTargetUri(targetUri); var tenantId = Guid.Empty; if (targetUri.Host.EndsWith(VstsBaseUrlHost, StringComparison.OrdinalIgnoreCase)) { Git.Trace.WriteLine($"'{targetUri}' is subdomain of '{VstsBaseUrlHost}', checking AAD vs MSA."); string tenant = null; WebResponse response; if (StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, "http") || StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, "https")) { // Query the cache first string tenantUrl = targetUri.ToString(); // Read the cache from disk var cache = await DeserializeTenantCache(); // Check the cache for an existing value if (cache.TryGetValue(tenantUrl, out tenantId)) { return(new KeyValuePair <bool, Guid>(true, tenantId)); } try { // build a request that we expect to fail, do not allow redirect to sign in url var request = WebRequest.CreateHttp(targetUri); request.UserAgent = Global.UserAgent; request.Method = "HEAD"; request.AllowAutoRedirect = false; // get the response from the server response = await request.GetResponseAsync(); } catch (WebException exception) { response = exception.Response; } // if the response exists and we have headers, parse them if (response != null && response.SupportsHeaders) { // find the VSTS resource tenant entry tenant = response.Headers[VstsResourceTenantHeader]; if (!String.IsNullOrWhiteSpace(tenant) && Guid.TryParse(tenant, out tenantId)) { // Update the cache. cache[tenantUrl] = tenantId; // Write the cache to disk. await SerializeTenantCache(cache); // Success, notify the caller return(new KeyValuePair <bool, Guid>(true, tenantId)); } } } else { Git.Trace.WriteLine($"detected non-https based protocol: {targetUri.Scheme}."); } } // if all else fails, fallback to basic authentication return(new KeyValuePair <bool, Guid>(false, tenantId)); }
/// <summary> /// Sets credentials for future use with this authentication object. /// </summary> /// <remarks>Not supported.</remarks> /// <param name="targetUri"> /// The uniform resource indicator of the resource access tokens are being set for. /// </param> /// <param name="credentials">The credentials being set.</param> public override void SetCredentials(TargetUri targetUri, Credential credentials) { BaseSecureStore.ValidateTargetUri(targetUri); BaseSecureStore.ValidateCredential(credentials); }
/// <summary> /// Validates that a `<see cref="Credential"/>` is valid to grant access to the VSTS resource referenced by `<paramref name="targetUri"/>`. /// <para/> /// Returns `<see langword="true"/>` if successful; otherwise `<see langword="false"/>`. /// </summary> /// <param name="targetUri">URI of the VSTS resource.</param> /// <param name="credentials">`<see cref="Credential"/>` expected to grant access to the VSTS service.</param> public async Task <bool> ValidateCredentials(TargetUri targetUri, Credential credentials) { BaseSecureStore.ValidateTargetUri(targetUri); BaseSecureStore.ValidateCredential(credentials); try { // Create an request to the VSTS deployment data end-point. HttpWebRequest request = GetConnectionDataRequest(targetUri, credentials); Git.Trace.WriteLine($"validating credentials against '{request.RequestUri}'."); // Send the request and wait for the response. using (HttpWebResponse response = await request.GetResponseAsync() as HttpWebResponse) { // We're looking for 'OK 200' here, anything else is failure Git.Trace.WriteLine($"server returned: ({response.StatusCode})."); return(response.StatusCode == HttpStatusCode.OK); } } catch (WebException webException) { // Avoid invalidation credentials based on what is likely a networking problem. switch (webException.Status) { case WebExceptionStatus.ConnectFailure: case WebExceptionStatus.ConnectionClosed: case WebExceptionStatus.NameResolutionFailure: case WebExceptionStatus.ProxyNameResolutionFailure: case WebExceptionStatus.ReceiveFailure: case WebExceptionStatus.RequestCanceled: case WebExceptionStatus.RequestProhibitedByCachePolicy: case WebExceptionStatus.RequestProhibitedByProxy: case WebExceptionStatus.SecureChannelFailure: case WebExceptionStatus.SendFailure: case WebExceptionStatus.TrustFailure: { Git.Trace.WriteLine($"unable to validate credentials due to '{webException.Status}'."); return(true); } } // Even if the service responded, if the issue isn't a 400 class response then // the credentials were likely not rejected. if (webException.Response is HttpWebResponse response) { int statusCode = (int)response.StatusCode; if (statusCode < 400 && statusCode >= 500) { Git.Trace.WriteLine($"server returned: ({statusCode})."); return(true); } } Git.Trace.WriteLine($"server returned: '{webException.Message}."); } catch { Git.Trace.WriteLine("! unexpected error"); } Git.Trace.WriteLine($"credential validation for '{targetUri}' failed."); return(false); }
/// <summary> /// Generates a personal access token for use with Visual Studio Online. /// </summary> /// <param name="targetUri"> /// The uniform resource indicator of the resource access tokens are being requested for. /// </param> /// <param name="accessToken"></param> /// <param name="tokenScope"></param> /// <param name="requireCompactToken"></param> /// <returns></returns> public async Task <Token> GeneratePersonalAccessToken(TargetUri targetUri, Token accessToken, VstsTokenScope tokenScope, bool requireCompactToken) { const string AccessTokenHeader = "Bearer"; BaseSecureStore.ValidateTargetUri(targetUri); BaseSecureStore.ValidateToken(accessToken); if (ReferenceEquals(tokenScope, null)) { throw new ArgumentNullException(nameof(tokenScope)); } Trace.WriteLine("VstsAzureAuthority::GeneratePersonalAccessToken"); try { // create a `HttpClient` with a minimum number of redirects, default creds, and a reasonable timeout (access token generation seems to hang occasionally) using (HttpClientHandler handler = new HttpClientHandler() { MaxAutomaticRedirections = 2, UseDefaultCredentials = true }) using (HttpClient httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromMilliseconds(RequestTimeout) }) { httpClient.DefaultRequestHeaders.Add("User-Agent", Global.UserAgent); switch (accessToken.Type) { case TokenType.Access: Trace.WriteLine(" using Azure access token to acquire personal access token"); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(AccessTokenHeader, accessToken.Value); break; case TokenType.Federated: Trace.WriteLine(" using federated authentication token to acquire personal access token"); httpClient.DefaultRequestHeaders.Add("Cookie", accessToken.Value); break; default: return(null); } if (await PopulateTokenTargetId(targetUri, accessToken)) { Uri requestUri; if (TryCreateRequestUri(targetUri, requireCompactToken, out requestUri)) { Trace.WriteLine(" request url is " + requestUri); using (StringContent content = GetAccessTokenRequestBody(targetUri, accessToken, tokenScope)) using (HttpResponseMessage response = await httpClient.PostAsync(requestUri, content)) { if (response.StatusCode == HttpStatusCode.OK) { string responseText = await response.Content.ReadAsStringAsync(); if (!String.IsNullOrWhiteSpace(responseText)) { // find the 'token : <value>' portion of the result content, if any Match tokenMatch = null; if ((tokenMatch = Regex.Match(responseText, @"\s*""token""\s*:\s*""([^\""]+)""\s*", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)).Success) { string tokenValue = tokenMatch.Groups[1].Value; Token token = new Token(tokenValue, TokenType.Personal); Trace.WriteLine(" personal access token acquisition succeeded."); return(token); } } } } } } } } catch { Trace.WriteLine(" an error occurred."); } Trace.WriteLine(" personal access token acquisition failed."); return(null); }
public static async Task <Guid?> DetectAuthority(RuntimeContext context, TargetUri targetUri) { const string VstsBaseUrlHost = "visualstudio.com"; const string VstsResourceTenantHeader = "X-VSS-ResourceTenant"; BaseSecureStore.ValidateTargetUri(targetUri); var tenantId = Guid.Empty; if (targetUri.Host.EndsWith(VstsBaseUrlHost, StringComparison.OrdinalIgnoreCase)) { context.Trace.WriteLine($"'{targetUri}' is subdomain of '{VstsBaseUrlHost}', checking AAD vs MSA."); string tenant = null; if (StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, Uri.UriSchemeHttp) || StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, Uri.UriSchemeHttps)) { // Query the cache first. string tenantUrl = targetUri.ToString(); // Read the cache from disk. var cache = await DeserializeTenantCache(context); // Check the cache for an existing value. if (cache.TryGetValue(tenantUrl, out tenantId)) { return(tenantId); } var options = new NetworkRequestOptions(false) { Flags = NetworkRequestOptionFlags.UseProxy, Timeout = TimeSpan.FromMilliseconds(Global.RequestTimeout), }; try { using (var response = await context.Network.HttpGetAsync(targetUri, options)) { if (response.IsSuccessStatusCode) { if (response.Headers.TryGetValues(VstsResourceTenantHeader, out IEnumerable <string> values)) { tenant = System.Linq.Enumerable.First(values); if (!string.IsNullOrWhiteSpace(tenant) && Guid.TryParse(tenant, out tenantId)) { // Update the cache. cache[tenantUrl] = tenantId; // Write the cache to disk. await SerializeTenantCache(context, cache); // Success, notify the caller return(tenantId); } } } else { context.Trace.WriteLine($"unable to get response from '{targetUri}', server responded with '{(int)response.StatusCode} {response.StatusCode}'."); } } } catch (HttpRequestException exception) { context.Trace.WriteLine($"unable to get response from '{targetUri}' due to '{exception.Message}'."); } } else { context.Trace.WriteLine($"detected non-https based protocol: '{targetUri.Scheme}'."); } } // Fallback to basic authentication. return(null); }
/// <summary> /// Deletes `<see cref="Credential"/>` from the storage used by the authentication object. /// </summary> /// <param name="targetUri">The uniform resource indicator used to uniquely identify the credentials.</param> public override async Task <bool> DeleteCredentials(TargetUri targetUri) { BaseSecureStore.ValidateTargetUri(targetUri); return(await _credentialStore.DeleteCredentials(targetUri)); }
/// <summary> /// Gets `<see cref="Credential"/>` from the storage used by the authentication object. /// <para/> /// Returns a `<see cref="Credential"/>` if successful; otherwise `<see langword="null"/>`. /// </summary> /// <param name="targetUri">The uniform resource indicator used to uniquely identify the credentials.</param> public override async Task <Credential> GetCredentials(TargetUri targetUri) { BaseSecureStore.ValidateTargetUri(targetUri); return(await _credentialStore.ReadCredentials(targetUri)); }