internal async Task <TargetUri> GetIdentityServiceUri(TargetUri targetUri, Secret authorization) { const string LocationServiceUrlPathAndQuery = "_apis/ServiceDefinitions/LocationService2/951917AC-A960-4999-8464-E3F0AA25B381?api-version=1.0"; if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (authorization is null) { throw new ArgumentNullException(nameof(authorization)); } string tenantUrl = GetTargetUrl(targetUri, false); var locationServiceUrl = tenantUrl + LocationServiceUrlPathAndQuery; var requestUri = targetUri.CreateWith(queryUrl: locationServiceUrl); var options = new NetworkRequestOptions(true) { Authorization = authorization, }; try { using (var response = await Network.HttpGetAsync(requestUri, options)) { if (response.IsSuccessStatusCode) { using (HttpContent content = response.Content) { string responseText = await content.ReadAsStringAsync(); Match match; if ((match = Regex.Match(responseText, @"\""location\""\:\""([^\""]+)\""", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)).Success) { string identityServiceUrl = match.Groups[1].Value; var idenitityServiceUri = new Uri(identityServiceUrl, UriKind.Absolute); return(targetUri.CreateWith(idenitityServiceUri)); } } } Trace.WriteLine($"failed to find Identity Service for '{targetUri}' via location service [{(int)response.StatusCode} {response.ReasonPhrase}]."); } } catch (Exception exception) { Trace.WriteException(exception); throw new LocationServiceException($"Helper for `{targetUri}`.", exception); } return(null); }
private async Task <TargetUri> CreatePersonalAccessTokenRequestUri(TargetUri targetUri, Secret authorization, bool requireCompactToken) { const string SessionTokenUrl = "_apis/token/sessiontokens?api-version=1.0"; const string CompactTokenUrl = SessionTokenUrl + "&tokentype=compact"; if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (authorization is null) { throw new ArgumentNullException(nameof(authorization)); } var idenityServiceUri = await GetIdentityServiceUri(targetUri, authorization); if (idenityServiceUri is null) { throw new LocationServiceException($"Failed to find Identity Service for `{targetUri}`."); } string url = idenityServiceUri.ToString(); url += requireCompactToken ? CompactTokenUrl : SessionTokenUrl; var requestUri = new Uri(url, UriKind.Absolute); return(targetUri.CreateWith(requestUri)); }
public async Task <AuthenticationResult> TryGetUser(TargetUri targetUri, int requestTimeout, Uri restRootUrl, Secret authorization) { var options = new NetworkRequestOptions(true) { Authorization = authorization, Timeout = TimeSpan.FromMilliseconds(requestTimeout), }; var apiUrl = new Uri(restRootUrl, UserUrl); var requestUri = targetUri.CreateWith(apiUrl); using (var response = await Network.HttpGetAsync(requestUri, options)) { Trace.WriteLine($"server responded with {response.StatusCode}."); switch (response.StatusCode) { case HttpStatusCode.OK: case HttpStatusCode.Created: { Trace.WriteLine("authentication success: new password token created."); // Get username to cross check against supplied one var responseText = response.Content.AsString; var username = FindUsername(responseText); return(new AuthenticationResult(AuthenticationResultType.Success, username)); } case HttpStatusCode.Forbidden: { // A 403/Forbidden response indicates the username/password are // recognized and good but 2FA is on in which case we want to // indicate that with the TwoFactor result Trace.WriteLine("two-factor app authentication code required"); return(new AuthenticationResult(AuthenticationResultType.TwoFactor)); } case HttpStatusCode.Unauthorized: { // username or password are wrong. Trace.WriteLine("authentication unauthorized"); return(new AuthenticationResult(AuthenticationResultType.Failure)); } default: // any unexpected result can be treated as a failure. Trace.WriteLine("authentication failed"); return(new AuthenticationResult(AuthenticationResultType.Failure)); } } }
/// <summary> /// Use a refresh_token to get a new access_token /// </summary> /// <param name="targetUri"></param> /// <param name="currentRefreshToken"></param> /// <returns></returns> private async Task <AuthenticationResult> RefreshAccessToken(TargetUri targetUri, string currentRefreshToken) { if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (currentRefreshToken is null) { throw new ArgumentNullException(nameof(currentRefreshToken)); } var refreshUri = GetRefreshUri(); var requestUri = targetUri.CreateWith(refreshUri); var options = new NetworkRequestOptions(true) { Timeout = TimeSpan.FromMilliseconds(RequestTimeout), }; var content = GetRefreshRequestContent(currentRefreshToken); using (var response = await Network.HttpPostAsync(requestUri, content, options)) { Trace.WriteLine($"server responded with {response.StatusCode}."); switch (response.StatusCode) { case HttpStatusCode.OK: case HttpStatusCode.Created: { // the request was successful, look for the tokens in the response string responseText = await response.Content.ReadAsStringAsync(); var token = FindAccessToken(responseText); var refreshToken = FindRefreshToken(responseText); return(GetAuthenticationResult(token, refreshToken)); } case HttpStatusCode.Unauthorized: { // do something return(new AuthenticationResult(AuthenticationResultType.Failure)); } default: Trace.WriteLine("authentication failed"); var error = response.Content.ReadAsStringAsync(); return(new AuthenticationResult(AuthenticationResultType.Failure)); } } }
internal static TargetUri GetConnectionDataUri(TargetUri targetUri) { const string VstsValidationUrlPath = "_apis/connectiondata"; if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } // Create a URL to the connection data end-point, it's deployment level and "always on". string requestUrl = GetTargetUrl(targetUri, false); string validationUrl = requestUrl + VstsValidationUrlPath; return(targetUri.CreateWith(validationUrl)); }
public static async Task <Guid?> DetectAuthority(RuntimeContext context, TargetUri targetUri) { const int GuidStringLength = 36; const string XvssResourceTenantHeader = "X-VSS-ResourceTenant"; if (context is null) { throw new ArgumentNullException(nameof(context)); } if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } // Assume Azure DevOps using Azure "common tenant" (empty GUID). var tenantId = Guid.Empty; // Compose the request Uri, by default it is the target Uri. var requestUri = targetUri; // Override the request Uri, when actual Uri exists, with actual Uri. if (targetUri.ActualUri != null) { requestUri = targetUri.CreateWith(queryUri: targetUri.ActualUri); } // If the protocol (aka scheme) being used isn't HTTP based, there's no point in // querying the server, so skip that work. if (OrdinalIgnoreCase.Equals(requestUri.Scheme, Uri.UriSchemeHttp) || OrdinalIgnoreCase.Equals(requestUri.Scheme, Uri.UriSchemeHttps)) { var requestUrl = GetTargetUrl(requestUri, false); // Read the cache from disk. var cache = await DeserializeTenantCache(context); // Check the cache for an existing value. if (cache.TryGetValue(requestUrl, out tenantId)) { context.Trace.WriteLine($"'{requestUrl}' is Azure DevOps, tenant resource is {{{tenantId.ToString("N")}}}."); return(tenantId); } var options = new NetworkRequestOptions(false) { Flags = NetworkRequestOptionFlags.UseProxy, Timeout = TimeSpan.FromMilliseconds(Global.RequestTimeout), }; try { // Query the host use the response headers to determine if the host is Azure DevOps or not. using (var response = await context.Network.HttpHeadAsync(requestUri, options)) { if (response.Headers != null) { // If the "X-VSS-ResourceTenant" was returned, then it is Azure DevOps and we'll need it's value. if (response.Headers.TryGetValues(XvssResourceTenantHeader, out IEnumerable <string> values)) { context.Trace.WriteLine($"detected '{requestUrl}' as Azure DevOps from GET response."); // The "Www-Authenticate" is a more reliable header, because it indicates the // authentication scheme that should be used to access the requested entity. if (response.Headers.WwwAuthenticate != null) { foreach (var header in response.Headers.WwwAuthenticate) { const string AuthorizationUriPrefix = "authorization_uri="; var value = header.Parameter; if (value.Length >= AuthorizationUriPrefix.Length + AuthorityHostUrlBase.Length + GuidStringLength) { // The header parameter will look something like "authorization_uri=https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47" // and all we want is the portion after the '=' and before the last '/'. int index1 = value.IndexOf('=', AuthorizationUriPrefix.Length - 1); int index2 = value.LastIndexOf('/'); // Parse the header value if the necessary characters exist... if (index1 > 0 && index2 > index1) { var authorityUrl = value.Substring(index1 + 1, index2 - index1 - 1); var guidString = value.Substring(index2 + 1, GuidStringLength); // If the authority URL is as expected, attempt to parse the tenant resource identity. if (OrdinalIgnoreCase.Equals(authorityUrl, AuthorityHostUrlBase) && Guid.TryParse(guidString, out tenantId)) { // Update the cache. cache[requestUrl] = tenantId; // Write the cache to disk. await SerializeTenantCache(context, cache); // Since we found a value, break the loop (likely a loop of one item anyways). break; } } } } } else { // Since there wasn't a "Www-Authenticate" header returned // iterate through the values, taking the first non-zero value. foreach (string value in values) { // Try to find a value for the resource-tenant identity. // Given that some projects will return multiple tenant identities, if (!string.IsNullOrWhiteSpace(value) && Guid.TryParse(value, out tenantId)) { // Update the cache. cache[requestUrl] = tenantId; // Write the cache to disk. await SerializeTenantCache(context, cache); // Break the loop if a non-zero value has been detected. if (tenantId != Guid.Empty) { break; } } } } context.Trace.WriteLine($"tenant resource for '{requestUrl}' is {{{tenantId.ToString("N")}}}."); // Return the tenant identity to the caller because this is Azure DevOps. return(tenantId); } } else { context.Trace.WriteLine($"unable to get response from '{requestUri}' [{(int)response.StatusCode} {response.StatusCode}]."); } } } catch (HttpRequestException exception) { context.Trace.WriteLine($"unable to get response from '{requestUri}', an error occurred before the server could respond."); context.Trace.WriteException(exception); } } else { context.Trace.WriteLine($"detected non-http(s) based protocol: '{requestUri.Scheme}'."); } if (OrdinalIgnoreCase.Equals(VstsBaseUrlHost, requestUri.Host)) { return(Guid.Empty); } // Fallback to basic authentication. return(null); }
public static async Task <Guid?> DetectAuthority(RuntimeContext context, TargetUri targetUri) { const string VstsResourceTenantHeader = "X-VSS-ResourceTenant"; if (context is null) { throw new ArgumentNullException(nameof(context)); } if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } var tenantId = Guid.Empty; if (IsVstsUrl(targetUri)) { var tenantUrl = GetTargetUrl(targetUri, false); context.Trace.WriteLine($"'{targetUri}' is a member '{tenantUrl}', checking AAD vs MSA."); if (OrdinalIgnoreCase.Equals(targetUri.Scheme, Uri.UriSchemeHttp) || OrdinalIgnoreCase.Equals(targetUri.Scheme, Uri.UriSchemeHttps)) { // 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 { var tenantUri = targetUri.CreateWith(tenantUrl); using (var response = await context.Network.HttpHeadAsync(tenantUri, options)) { if (response.Headers != null && response.Headers.TryGetValues(VstsResourceTenantHeader, out IEnumerable <string> values)) { foreach (string value in values) { // Try to find a non-empty value for the resource-tenant identity if (!string.IsNullOrWhiteSpace(value) && Guid.TryParse(value, out tenantId) && tenantId != Guid.Empty) { // Update the cache. cache[tenantUrl] = tenantId; // Write the cache to disk. await SerializeTenantCache(context, cache); // Success, notify the caller return(tenantId); } } // Since we did not find a better identity, fallback to the default (Guid.Empty). return(tenantId); } else { context.Trace.WriteLine($"unable to get response from '{targetUri}' [{(int)response.StatusCode} {response.StatusCode}]."); } } } catch (HttpRequestException exception) { context.Trace.WriteLine($"unable to get response from '{targetUri}', an error occurred before the server could respond."); context.Trace.WriteException(exception); } } else { context.Trace.WriteLine($"detected non-http(s) based protocol: '{targetUri.Scheme}'."); } } if (StringComparer.OrdinalIgnoreCase.Equals(VstsBaseUrlHost, targetUri.Host)) { return(Guid.Empty); } // Fallback to basic authentication. return(null); }
public async Task <AuthenticationResult> AcquireToken( TargetUri targetUri, string username, string password, string authenticationCode, TokenScope scope) { const string GitHubOptHeader = "X-GitHub-OTP"; Token token = null; var options = new NetworkRequestOptions(true) { Authorization = new Credential(username, password), Timeout = TimeSpan.FromMilliseconds(RequestTimeout), }; options.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(GitHubApiAcceptsHeaderValue)); options.Headers.Add(GitHubOptHeader, authenticationCode); // Create the authority Uri. var requestUri = targetUri.CreateWith(_authorityUrl); using (HttpContent content = GetTokenJsonContent(targetUri, scope)) using (var response = await Network.HttpPostAsync(requestUri, content, options)) { Trace.WriteLine($"server responded with {response.StatusCode}."); switch (response.StatusCode) { case HttpStatusCode.OK: case HttpStatusCode.Created: { string responseText = await response.Content.ReadAsStringAsync(); Match tokenMatch; if ((tokenMatch = Regex.Match(responseText, @"\s*""token""\s*:\s*""([^""]+)""\s*", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)).Success && tokenMatch.Groups.Count > 1) { string tokenText = tokenMatch.Groups[1].Value; token = new Token(tokenText, TokenType.Personal); } if (token == null) { Trace.WriteLine($"authentication for '{targetUri}' failed."); return(new AuthenticationResult(GitHubAuthenticationResultType.Failure)); } else { Trace.WriteLine($"authentication success: new personal access token for '{targetUri}' created."); return(new AuthenticationResult(GitHubAuthenticationResultType.Success, token)); } } case HttpStatusCode.Unauthorized: { if (string.IsNullOrWhiteSpace(authenticationCode) && response.Headers.Any(x => string.Equals(GitHubOptHeader, x.Key, StringComparison.OrdinalIgnoreCase))) { var mfakvp = response.Headers.First(x => string.Equals(GitHubOptHeader, x.Key, StringComparison.OrdinalIgnoreCase) && x.Value != null && x.Value.Count() > 0); if (mfakvp.Value.First().Contains("app")) { Trace.WriteLine($"two-factor app authentication code required for '{targetUri}'."); return(new AuthenticationResult(GitHubAuthenticationResultType.TwoFactorApp)); } else { Trace.WriteLine($"two-factor sms authentication code required for '{targetUri}'."); return(new AuthenticationResult(GitHubAuthenticationResultType.TwoFactorSms)); } } else { Trace.WriteLine($"authentication failed for '{targetUri}'."); return(new AuthenticationResult(GitHubAuthenticationResultType.Failure)); } } case HttpStatusCode.Forbidden: // This API only supports Basic authentication. If a valid OAuth token is supplied // as the password, then a Forbidden response is returned instead of an Unauthorized. // In that case, the supplied password is an OAuth token and is valid and we don't need // to create a new personal access token. var contentBody = await response.Content.ReadAsStringAsync(); if (contentBody.Contains("This API can only be accessed with username and password Basic Auth")) { Trace.WriteLine($"authentication success: user supplied personal access token for '{targetUri}'."); return(new AuthenticationResult(GitHubAuthenticationResultType.Success, new Token(password, TokenType.Personal))); } Trace.WriteLine($"authentication failed for '{targetUri}'."); return(new AuthenticationResult(GitHubAuthenticationResultType.Failure)); default: Trace.WriteLine($"authentication failed for '{targetUri}'."); return(new AuthenticationResult(GitHubAuthenticationResultType.Failure)); } } }