using System; using System.Linq; using System.Net; using GitHub.Services.Common; using GitHub.Services.Common.Internal; namespace GitHub.Services.Client { /// /// Provides federated authentication with a hosted VssConnection instance using cookies. /// [Serializable] public sealed class VssFederatedCredential : FederatedCredential { /// /// Initializes a new VssFederatedCredential instance. /// public VssFederatedCredential() : this(true) { } /// /// Initializes a new VssFederatedCredential instance. /// public VssFederatedCredential(Boolean useCache) : this(useCache, null) { } /// /// Initializes a new VssFederatedCredential instance. /// /// The initial token if available public VssFederatedCredential(VssFederatedToken initialToken) : this(false, initialToken) { } public VssFederatedCredential( Boolean useCache, VssFederatedToken initialToken) : base(initialToken) { #if !NETSTANDARD if (useCache) { Storage = new VssClientCredentialStorage(); } #endif } /// /// /// public override VssCredentialsType CredentialType { get { return VssCredentialsType.Federated; } } public override Boolean IsAuthenticationChallenge(IHttpResponse webResponse) { bool isNonAuthenticationChallenge = false; return IsVssFederatedAuthenticationChallenge(webResponse, out isNonAuthenticationChallenge) ?? isNonAuthenticationChallenge; } protected override IssuedTokenProvider OnCreateTokenProvider( Uri serverUrl, IHttpResponse response) { // The response is only null when attempting to determine the most appropriate token provider to // use for the connection. The only way we should do anything here is if we have an initial token // since that means we can present something without making a server call. if (response == null && base.InitialToken == null) { return null; } Uri signInUrl = null; String realm = String.Empty; String issuer = String.Empty; if (response != null) { var location = response.Headers.GetValues(HttpHeaders.Location).FirstOrDefault(); if (location == null) { location = response.Headers.GetValues(HttpHeaders.TfsFedAuthRedirect).FirstOrDefault(); } if (!String.IsNullOrEmpty(location)) { signInUrl = new Uri(location); } // Inform the server that we support the javascript notify "smart client" pattern for ACS auth AddParameter(ref signInUrl, "protocol", "javascriptnotify"); // Do not automatically sign in with existing FedAuth cookie AddParameter(ref signInUrl, "force", "1"); GetRealmAndIssuer(response, out realm, out issuer); } return new VssFederatedTokenProvider(this, serverUrl, signInUrl, issuer, realm); } internal static void GetRealmAndIssuer( IHttpResponse response, out String realm, out String issuer) { realm = response.Headers.GetValues(HttpHeaders.TfsFedAuthRealm).FirstOrDefault(); issuer = response.Headers.GetValues(HttpHeaders.TfsFedAuthIssuer).FirstOrDefault(); if (!String.IsNullOrWhiteSpace(issuer)) { issuer = new Uri(issuer).GetLeftPart(UriPartial.Authority); } } internal static Boolean? IsVssFederatedAuthenticationChallenge( IHttpResponse webResponse, out Boolean isNonAuthenticationChallenge) { isNonAuthenticationChallenge = false; if (webResponse == null) { return false; } // Check to make sure that the redirect was issued from the Tfs service. We include the TfsServiceError // header to avoid the possibility that a redirect from a non-tfs service is issued and we incorrectly // launch the credentials UI. if (webResponse.StatusCode == HttpStatusCode.Found || webResponse.StatusCode == HttpStatusCode.Redirect) { return webResponse.Headers.GetValues(HttpHeaders.Location).Any() && webResponse.Headers.GetValues(HttpHeaders.TfsFedAuthRealm).Any(); } else if (webResponse.StatusCode == HttpStatusCode.Unauthorized) { return webResponse.Headers.GetValues(HttpHeaders.WwwAuthenticate).Any(x => x.StartsWith("TFS-Federated", StringComparison.OrdinalIgnoreCase)); } else if (webResponse.StatusCode == HttpStatusCode.Forbidden) { // This is not strictly an "authentication challenge" but it is a state the user can do something about so they can get access to the resource // they are attempting to access. Specifically, the user will hit this when they need to update or create a profile required by business policy. isNonAuthenticationChallenge = webResponse.Headers.GetValues(HttpHeaders.TfsFedAuthRedirect).Any(); if (isNonAuthenticationChallenge) { return null; } } return false; } private static void AddParameter(ref Uri uri, String name, String value) { if (uri.Query.IndexOf(String.Concat(name, "="), StringComparison.OrdinalIgnoreCase) < 0) { UriBuilder builder = new UriBuilder(uri); builder.Query = String.Concat(builder.Query.TrimStart('?'), "&", name, "=", value); uri = builder.Uri; } } } }