This commit is contained in:
Tingluo Huang
2019-12-12 14:42:02 -05:00
parent 3ea3b5ff59
commit 1404a73762
13 changed files with 1 additions and 1185 deletions

View File

@@ -158,7 +158,6 @@ namespace GitHub.Runner.Common
public static class Configuration
{
public static readonly string AAD = "AAD";
public static readonly string OAuthAccessToken = "OAuthAccessToken";
public static readonly string OAuth = "OAuth";
}

View File

@@ -518,7 +518,7 @@ namespace GitHub.Runner.Listener.Configuration
Trace.Info(nameof(GetCredentialProvider));
var credentialManager = HostContext.GetService<ICredentialManager>();
string authType = command.GetAuth(defaultValue: Constants.Configuration.AAD);
string authType = command.GetAuth(defaultValue: Constants.Configuration.OAuthAccessToken);
// Create the credential.
Trace.Info("Creating credential for auth: {0}", authType);

View File

@@ -20,7 +20,6 @@ namespace GitHub.Runner.Listener.Configuration
{
public static readonly Dictionary<string, Type> CredentialTypes = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
{
{ Constants.Configuration.AAD, typeof(AadDeviceCodeAccessToken)},
{ Constants.Configuration.OAuth, typeof(OAuthCredential)},
{ Constants.Configuration.OAuthAccessToken, typeof(OAuthAccessTokenCredential)},
};

View File

@@ -37,125 +37,6 @@ namespace GitHub.Runner.Listener.Configuration
public abstract void EnsureCredential(IHostContext context, CommandSettings command, string serverUrl);
}
public sealed class AadDeviceCodeAccessToken : CredentialProvider
{
private string _azureDevOpsClientId = "97877f11-0fc6-4aee-b1ff-febb0519dd00";
public override Boolean RequireInteractive => true;
public AadDeviceCodeAccessToken() : base(Constants.Configuration.AAD) { }
public override VssCredentials GetVssCredentials(IHostContext context)
{
ArgUtil.NotNull(context, nameof(context));
Tracing trace = context.GetTrace(nameof(AadDeviceCodeAccessToken));
trace.Info(nameof(GetVssCredentials));
ArgUtil.NotNull(CredentialData, nameof(CredentialData));
CredentialData.Data.TryGetValue(Constants.Runner.CommandLine.Args.Url, out string serverUrl);
ArgUtil.NotNullOrEmpty(serverUrl, nameof(serverUrl));
var tenantAuthorityUrl = GetTenantAuthorityUrl(context, serverUrl);
if (tenantAuthorityUrl == null)
{
throw new NotSupportedException($"'{serverUrl}' is not backed by Azure Active Directory.");
}
LoggerCallbackHandler.LogCallback = ((LogLevel level, string message, bool containsPii) =>
{
switch (level)
{
case LogLevel.Information:
trace.Info(message);
break;
case LogLevel.Error:
trace.Error(message);
break;
case LogLevel.Warning:
trace.Warning(message);
break;
default:
trace.Verbose(message);
break;
}
});
LoggerCallbackHandler.UseDefaultLogging = false;
AuthenticationContext ctx = new AuthenticationContext(tenantAuthorityUrl.AbsoluteUri);
var queryParameters = $"redirect_uri={Uri.EscapeDataString(new Uri(serverUrl).GetLeftPart(UriPartial.Authority))}";
DeviceCodeResult codeResult = ctx.AcquireDeviceCodeAsync("https://management.core.windows.net/", _azureDevOpsClientId, queryParameters).GetAwaiter().GetResult();
var term = context.GetService<ITerminal>();
term.WriteLine($"Please finish AAD device code flow in browser ({codeResult.VerificationUrl}), user code: {codeResult.UserCode}");
if (string.Equals(CredentialData.Data[Constants.Runner.CommandLine.Flags.LaunchBrowser], bool.TrueString, StringComparison.OrdinalIgnoreCase))
{
try
{
#if OS_WINDOWS
Process.Start(new ProcessStartInfo() { FileName = codeResult.VerificationUrl, UseShellExecute = true });
#elif OS_LINUX
Process.Start(new ProcessStartInfo() { FileName = "xdg-open", Arguments = codeResult.VerificationUrl });
#else
Process.Start(new ProcessStartInfo() { FileName = "open", Arguments = codeResult.VerificationUrl });
#endif
}
catch (Exception ex)
{
// not able to open browser, ex: xdg-open/open is not installed.
trace.Error(ex);
term.WriteLine($"Fail to open browser. {codeResult.Message}");
}
}
AuthenticationResult authResult = ctx.AcquireTokenByDeviceCodeAsync(codeResult).GetAwaiter().GetResult();
ArgUtil.NotNull(authResult, nameof(authResult));
trace.Info($"receive AAD auth result with {authResult.AccessTokenType} token");
var aadCred = new VssAadCredential(new VssAadToken(authResult));
VssCredentials creds = new VssCredentials(null, aadCred, CredentialPromptType.DoNotPrompt);
trace.Info("cred created");
return creds;
}
public override void EnsureCredential(IHostContext context, CommandSettings command, string serverUrl)
{
ArgUtil.NotNull(context, nameof(context));
Tracing trace = context.GetTrace(nameof(AadDeviceCodeAccessToken));
trace.Info(nameof(EnsureCredential));
ArgUtil.NotNull(command, nameof(command));
CredentialData.Data[Constants.Runner.CommandLine.Args.Url] = serverUrl;
CredentialData.Data[Constants.Runner.CommandLine.Flags.LaunchBrowser] = command.GetAutoLaunchBrowser().ToString();
}
private Uri GetTenantAuthorityUrl(IHostContext context, string serverUrl)
{
using (var client = new HttpClient(context.CreateHttpClientHandler()))
{
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("X-TFS-FedAuthRedirect", "Suppress");
client.DefaultRequestHeaders.UserAgent.Clear();
client.DefaultRequestHeaders.UserAgent.AddRange(VssClientHttpRequestSettings.Default.UserAgent);
var requestMessage = new HttpRequestMessage(HttpMethod.Head, $"{serverUrl.Trim('/')}/_apis/connectiondata");
var response = client.SendAsync(requestMessage).GetAwaiter().GetResult();
// Get the tenant from the Login URL, MSA backed accounts will not return `Bearer` www-authenticate header.
var bearerResult = response.Headers.WwwAuthenticate.Where(p => p.Scheme.Equals("Bearer", StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
if (bearerResult != null && bearerResult.Parameter.StartsWith("authorization_uri=", StringComparison.OrdinalIgnoreCase))
{
var authorizationUri = bearerResult.Parameter.Substring("authorization_uri=".Length);
if (Uri.TryCreate(authorizationUri, UriKind.Absolute, out Uri aadTenantUrl))
{
return aadTenantUrl;
}
}
return null;
}
}
}
public sealed class OAuthAccessTokenCredential : CredentialProvider
{
public OAuthAccessTokenCredential() : base(Constants.Configuration.OAuthAccessToken) { }

View File

@@ -24,7 +24,6 @@
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.4.0" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.4.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="4.4.0" />
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="3.19.4" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

View File

@@ -1,264 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text;
using GitHub.Services.Common;
namespace GitHub.Services.Client
{
internal static class CookieUtility
{
public static readonly String AcsMetadataRetrievalExceptionText = "Unable to retrieve ACS Metadata from '{0}'";
public static readonly String FedAuthCookieName = "FedAuth";
public static readonly String WindowsLiveSignOutUrl = "https://login.live.com/uilogout.srf";
public static readonly Uri WindowsLiveCookieDomain = new Uri("https://login.live.com/");
public static CookieCollection GetFederatedCookies(Uri cookieDomainAndPath)
{
CookieCollection result = null;
Cookie cookie = GetCookieEx(cookieDomainAndPath, FedAuthCookieName).FirstOrDefault();
if (cookie != null)
{
result = new CookieCollection();
result.Add(cookie);
for (Int32 x = 1; x < 50; x++)
{
String cookieName = FedAuthCookieName + x;
cookie = GetCookieEx(cookieDomainAndPath, cookieName).FirstOrDefault();
if (cookie != null)
{
result.Add(cookie);
}
else
{
break;
}
}
}
return result;
}
public static CookieCollection GetFederatedCookies(String[] token)
{
CookieCollection result = null;
if (token != null && token.Length > 0 && token[0] != null)
{
result = new CookieCollection();
result.Add(new Cookie(FedAuthCookieName, token[0]));
for (Int32 x = 1; x < token.Length; x++)
{
String cookieName = FedAuthCookieName + x;
if (token[x] != null)
{
Cookie cookie = new Cookie(cookieName, token[x]);
cookie.HttpOnly = true;
result.Add(cookie);
}
else
{
break;
}
}
}
return result;
}
public static CookieCollection GetFederatedCookies(IHttpResponse webResponse)
{
CookieCollection result = null;
IEnumerable<String> cookies = null;
if (webResponse.Headers.TryGetValues("Set-Cookie", out cookies))
{
foreach (String cookie in cookies)
{
if (cookie != null && cookie.StartsWith(CookieUtility.FedAuthCookieName, StringComparison.OrdinalIgnoreCase))
{
// Only take the security token field of the cookie, and discard the rest
String fedAuthToken = cookie.Split(';').FirstOrDefault();
Int32 index = fedAuthToken.IndexOf('=');
if (index > 0 && index < fedAuthToken.Length - 1)
{
String name = fedAuthToken.Substring(0, index);
String value = fedAuthToken.Substring(index + 1);
result = result ?? new CookieCollection();
result.Add(new Cookie(name, value));
}
}
}
}
return result;
}
public static CookieCollection GetAllCookies(Uri cookieDomainAndPath)
{
CookieCollection result = null;
List<Cookie> cookies = GetCookieEx(cookieDomainAndPath, null);
foreach (Cookie cookie in cookies)
{
if (result == null)
{
result = new CookieCollection();
}
result.Add(cookie);
}
return result;
}
public static void DeleteFederatedCookies(Uri cookieDomainAndPath)
{
CookieCollection cookies = GetFederatedCookies(cookieDomainAndPath);
if (cookies != null)
{
foreach (Cookie cookie in cookies)
{
DeleteCookieEx(cookieDomainAndPath, cookie.Name);
}
}
}
public static void DeleteWindowsLiveCookies()
{
DeleteAllCookies(WindowsLiveCookieDomain);
}
public static void DeleteAllCookies(Uri cookieDomainAndPath)
{
CookieCollection cookies = GetAllCookies(cookieDomainAndPath);
if (cookies != null)
{
foreach (Cookie cookie in cookies)
{
DeleteCookieEx(cookieDomainAndPath, cookie.Name);
}
}
}
public const UInt32 INTERNET_COOKIE_HTTPONLY = 0x00002000;
[DllImport("wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern bool InternetGetCookieEx(
String url, String cookieName, StringBuilder cookieData, ref Int32 size, UInt32 flags, IntPtr reserved);
[DllImport("wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern bool InternetSetCookieEx(
String url, String cookieName, String cookieData, UInt32 flags, IntPtr reserved);
public static Boolean DeleteCookieEx(Uri cookiePath, String cookieName)
{
UInt32 flags = INTERNET_COOKIE_HTTPONLY;
String path = cookiePath.ToString();
if (!path.EndsWith("/", StringComparison.Ordinal))
{
path = path + "/";
}
DateTime expiration = DateTime.UtcNow.AddYears(-1);
String cookieData = String.Format(CultureInfo.InvariantCulture, "{0}=0;expires={1};path=/;domain={2};httponly", cookieName, expiration.ToString("R"), cookiePath.Host);
return InternetSetCookieEx(path, null, cookieData, flags, IntPtr.Zero);
}
public static Boolean SetCookiesEx(
Uri cookiePath,
CookieCollection cookies)
{
String path = cookiePath.ToString();
if (!path.EndsWith("/", StringComparison.Ordinal))
{
path = path + "/";
}
Boolean successful = true;
foreach (Cookie cookie in cookies)
{
// This means it doesn't expire
if (cookie.Expires.Year == 1)
{
continue;
}
String cookieData = String.Format(CultureInfo.InvariantCulture,
"{0}; path={1}; domain={2}; expires={3}; httponly",
cookie.Value,
cookie.Path,
cookie.Domain,
cookie.Expires.ToString("ddd, dd-MMM-yyyy HH:mm:ss 'GMT'"));
successful &= InternetSetCookieEx(path, cookie.Name, cookieData, INTERNET_COOKIE_HTTPONLY, IntPtr.Zero);
}
return successful;
}
public static List<Cookie> GetCookieEx(Uri cookiePath, String cookieName)
{
UInt32 flags = INTERNET_COOKIE_HTTPONLY;
List<Cookie> cookies = new List<Cookie>();
Int32 size = 256;
StringBuilder cookieData = new StringBuilder(size);
String path = cookiePath.ToString();
if (!path.EndsWith("/", StringComparison.Ordinal))
{
path = path + "/";
}
if (!InternetGetCookieEx(path, cookieName, cookieData, ref size, flags, IntPtr.Zero))
{
if (size < 0)
{
return cookies;
}
cookieData = new StringBuilder(size);
if (!InternetGetCookieEx(path, cookieName, cookieData, ref size, flags, IntPtr.Zero))
{
return cookies;
}
}
if (cookieData.Length > 0)
{
String[] cookieSections = cookieData.ToString().Split(new char[] { ';' });
foreach (String cookieSection in cookieSections)
{
String[] cookieParts = cookieSection.Split(new char[] { '=' }, 2);
if (cookieParts.Length == 2)
{
Cookie cookie = new Cookie();
cookie.Name = cookieParts[0].TrimStart();
cookie.Value = cookieParts[1];
cookie.HttpOnly = true;
cookies.Add(cookie);
}
}
}
return cookies;
}
}
}

View File

@@ -1,95 +0,0 @@
using System;
using System.Net.Http;
using System.Security;
using GitHub.Services.Common;
namespace GitHub.Services.Client
{
/// <summary>
/// Currently it is impossible to get whether prompting is allowed from the credential itself without reproducing the logic
/// used by VssClientCredentials. Since this is a stop gap solution to get Windows integrated authentication to work against
/// AAD via ADFS for now this class will only support that one, non-interactive flow. We need to assess how much we want to
/// invest in this legacy stack rather than recommending people move to the VssConnect API for future authentication needs.
/// </summary>
[Serializable]
public sealed class VssAadCredential : FederatedCredential
{
private string username;
private SecureString password;
public VssAadCredential()
: base(null)
{
}
public VssAadCredential(VssAadToken initialToken)
: base(initialToken)
{
}
public VssAadCredential(string username)
: base(null)
{
this.username = username;
}
public VssAadCredential(string username, string password)
: base(null)
{
this.username = username;
if (password != null)
{
this.password = new SecureString();
foreach (char character in password)
{
this.password.AppendChar(character);
}
}
}
public VssAadCredential(string username, SecureString password)
: base(null)
{
this.username = username;
this.password = password;
}
public override VssCredentialsType CredentialType
{
get
{
return VssCredentialsType.Aad;
}
}
internal string Username
{
get
{
return username;
}
}
internal SecureString Password => password;
public override bool IsAuthenticationChallenge(IHttpResponse webResponse)
{
bool isNonAuthenticationChallenge = false;
return VssFederatedCredential.IsVssFederatedAuthenticationChallenge(webResponse, out isNonAuthenticationChallenge) ?? false;
}
protected override IssuedTokenProvider OnCreateTokenProvider(
Uri serverUrl,
IHttpResponse response)
{
if (response == null && base.InitialToken == null)
{
return null;
}
return new VssAadTokenProvider(this);
}
}
}

View File

@@ -1,89 +0,0 @@
using System;
using System.Diagnostics;
using GitHub.Services.WebApi;
using GitHub.Services.WebApi.Internal;
namespace GitHub.Services.Client
{
internal static class VssAadSettings
{
public const string DefaultAadInstance = "https://login.microsoftonline.com/";
public const string CommonTenant = "common";
// VSTS service principal.
public const string Resource = "499b84ac-1321-427f-aa17-267ca6975798";
// Visual Studio IDE client ID originally provisioned by Azure Tools.
public const string Client = "872cd9fa-d31f-45e0-9eab-6e460a02d1f1";
// AAD Production Application tenant.
private const string ApplicationTenantId = "f8cdef31-a31e-4b4a-93e4-5f571e91255a";
#if !NETSTANDARD
public static Uri NativeClientRedirectUri
{
get
{
Uri nativeClientRedirect = null;
try
{
string nativeRedirect = VssClientEnvironment.GetSharedConnectedUserValue<string>(VssConnectionParameterOverrideKeys.AadNativeClientRedirect);
if (!string.IsNullOrEmpty(nativeRedirect))
{
Uri.TryCreate(nativeRedirect, UriKind.RelativeOrAbsolute, out nativeClientRedirect);
}
}
catch (Exception e)
{
Debug.WriteLine(string.Format("NativeClientRedirectUri: {0}", e));
}
return nativeClientRedirect ?? new Uri("urn:ietf:wg:oauth:2.0:oob");
}
}
public static string ClientId
{
get
{
string nativeRedirect = VssClientEnvironment.GetSharedConnectedUserValue<string>(VssConnectionParameterOverrideKeys.AadNativeClientIdentifier);
return nativeRedirect ?? VssAadSettings.Client;
}
}
#endif
public static string AadInstance
{
get
{
#if !NETSTANDARD
string aadInstance = VssClientEnvironment.GetSharedConnectedUserValue<string>(VssConnectionParameterOverrideKeys.AadInstance);
#else
string aadInstance = null;
#endif
if (string.IsNullOrWhiteSpace(aadInstance))
{
aadInstance = DefaultAadInstance;
}
else if (!aadInstance.EndsWith("/"))
{
aadInstance = aadInstance + "/";
}
return aadInstance;
}
}
#if !NETSTANDARD
/// <summary>
/// Application tenant either from a registry override or a constant
/// </summary>
public static string ApplicationTenant =>
VssClientEnvironment.GetSharedConnectedUserValue<string>(VssConnectionParameterOverrideKeys.AadApplicationTenant)
?? VssAadSettings.ApplicationTenantId;
#endif
}
}

View File

@@ -1,124 +0,0 @@
using System;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using GitHub.Services.Common;
namespace GitHub.Services.Client
{
[Serializable]
public class VssAadToken : IssuedToken
{
private string accessToken;
private string accessTokenType;
private AuthenticationContext authenticationContext;
private UserCredential userCredential;
private VssAadTokenOptions options;
public VssAadToken(AuthenticationResult authentication)
{
// Prevent any attempt to store this token.
this.FromStorage = true;
if (!string.IsNullOrWhiteSpace(authentication.AccessToken))
{
this.Authenticated();
}
this.accessToken = authentication.AccessToken;
this.accessTokenType = authentication.AccessTokenType;
}
public VssAadToken(
string accessTokenType,
string accessToken)
{
// Prevent any attempt to store this token.
this.FromStorage = true;
if (!string.IsNullOrWhiteSpace(accessToken) && !string.IsNullOrWhiteSpace(accessTokenType))
{
this.Authenticated();
}
this.accessToken = accessToken;
this.accessTokenType = accessTokenType;
}
public VssAadToken(
AuthenticationContext authenticationContext,
UserCredential userCredential = null,
VssAadTokenOptions options = VssAadTokenOptions.None)
{
// Prevent any attempt to store this token.
this.FromStorage = true;
this.authenticationContext = authenticationContext;
this.userCredential = userCredential;
this.options = options;
}
protected internal override VssCredentialsType CredentialType
{
get
{
return VssCredentialsType.Aad;
}
}
public AuthenticationResult AcquireToken()
{
if (this.authenticationContext == null)
{
return null;
}
AuthenticationResult authenticationResult = null;
for (int index = 0; index < 3; index++)
{
try
{
if (this.userCredential == null && !options.HasFlag(VssAadTokenOptions.AllowDialog))
{
authenticationResult = authenticationContext.AcquireTokenSilentAsync(VssAadSettings.Resource, VssAadSettings.Client).ConfigureAwait(false).GetAwaiter().GetResult();
}
else
{
authenticationResult = authenticationContext.AcquireTokenAsync(VssAadSettings.Resource, VssAadSettings.Client, this.userCredential).ConfigureAwait(false).GetAwaiter().GetResult();
}
if (authenticationResult != null)
{
break;
}
}
catch (Exception x)
{
System.Diagnostics.Debug.WriteLine("Failed to get ADFS token: " + x.ToString());
}
}
return authenticationResult;
}
internal override void ApplyTo(IHttpRequest request)
{
AuthenticationResult authenticationResult = AcquireToken();
if (authenticationResult != null)
{
request.Headers.SetValue(Common.Internal.HttpHeaders.Authorization, $"{authenticationResult.AccessTokenType} {authenticationResult.AccessToken}");
}
else if (!string.IsNullOrEmpty(this.accessTokenType) && !string.IsNullOrEmpty(this.accessToken))
{
request.Headers.SetValue(Common.Internal.HttpHeaders.Authorization, $"{this.accessTokenType} {this.accessToken}");
}
}
}
[Flags]
public enum VssAadTokenOptions
{
None = 0,
AllowDialog = 1
}
}

View File

@@ -1,77 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using GitHub.Services.Common;
namespace GitHub.Services.Client
{
internal sealed class VssAadTokenProvider : IssuedTokenProvider
{
public VssAadTokenProvider(VssAadCredential credential)
: base(credential, null, null)
{
}
public override bool GetTokenIsInteractive
{
get
{
return false;
}
}
private VssAadToken GetVssAadToken()
{
AuthenticationContext authenticationContext = new AuthenticationContext(string.Concat(VssAadSettings.AadInstance, VssAadSettings.CommonTenant));
UserCredential userCredential = null;
VssAadCredential credential = this.Credential as VssAadCredential;
if (credential?.Username != null)
{
#if NETSTANDARD
// UserPasswordCredential does not currently exist for ADAL 3.13.5 for any non-desktop build.
userCredential = new UserCredential(credential.Username);
#else
if (credential.Password != null)
{
userCredential = new UserPasswordCredential(credential.Username, credential.Password);
}
else
{
userCredential = new UserCredential(credential.Username);
}
#endif
}
else
{
userCredential = new UserCredential();
}
return new VssAadToken(authenticationContext, userCredential);
}
/// <summary>
/// Temporary implementation since we don't have a good configuration story here at the moment.
/// </summary>
protected override Task<IssuedToken> OnGetTokenAsync(IssuedToken failedToken, CancellationToken cancellationToken)
{
// If we have already tried to authenticate with an AAD token retrieved from Windows integrated authentication and it is not working, clear out state.
if (failedToken != null && failedToken.CredentialType == VssCredentialsType.Aad && failedToken.IsAuthenticated)
{
this.CurrentToken = null;
return Task.FromResult<IssuedToken>(null);
}
try
{
return Task.FromResult<IssuedToken>(GetVssAadToken());
}
catch
{ }
return Task.FromResult<IssuedToken>(null);
}
}
}

View File

@@ -1,172 +0,0 @@
using System;
using System.Linq;
using System.Net;
using GitHub.Services.Common;
using GitHub.Services.Common.Internal;
namespace GitHub.Services.Client
{
/// <summary>
/// Provides federated authentication with a hosted <c>VssConnection</c> instance using cookies.
/// </summary>
[Serializable]
public sealed class VssFederatedCredential : FederatedCredential
{
/// <summary>
/// Initializes a new <c>VssFederatedCredential</c> instance.
/// </summary>
public VssFederatedCredential()
: this(true)
{
}
/// <summary>
/// Initializes a new <c>VssFederatedCredential</c> instance.
/// </summary>
public VssFederatedCredential(Boolean useCache)
: this(useCache, null)
{
}
/// <summary>
/// Initializes a new <c>VssFederatedCredential</c> instance.
/// </summary>
/// <param name="initialToken">The initial token if available</param>
public VssFederatedCredential(VssFederatedToken initialToken)
: this(false, initialToken)
{
}
public VssFederatedCredential(
Boolean useCache,
VssFederatedToken initialToken)
: base(initialToken)
{
#if !NETSTANDARD
if (useCache)
{
Storage = new VssClientCredentialStorage();
}
#endif
}
/// <summary>
///
/// </summary>
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;
}
}
}
}

View File

@@ -1,84 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using GitHub.Services.Common;
namespace GitHub.Services.Client
{
/// <summary>
/// Provides a cookie-based authentication token.
/// </summary>
[Serializable]
public sealed class VssFederatedToken : IssuedToken
{
/// <summary>
/// Initializes a new <c>VssFederatedToken</c> instance using the specified cookies.
/// </summary>
/// <param name="cookies"></param>
public VssFederatedToken(CookieCollection cookies)
{
ArgumentUtility.CheckForNull(cookies, "cookies");
m_cookies = cookies;
}
/// <summary>
/// Returns the CookieCollection contained within this token. For internal use only.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public CookieCollection CookieCollection
{
get
{
return m_cookies;
}
}
protected internal override VssCredentialsType CredentialType
{
get
{
return VssCredentialsType.Federated;
}
}
internal override void ApplyTo(IHttpRequest request)
{
// From http://www.ietf.org/rfc/rfc2109.txt:
// Note: For backward compatibility, the separator in the Cookie header
// is semi-colon (;) everywhere.
//
// HttpRequestHeaders uses comma as the default separator, so instead of returning
// a list of cookies, the method returns one semicolon separated string.
IEnumerable<String> values = request.Headers.GetValues(s_cookieHeader);
request.Headers.SetValue(s_cookieHeader, GetHeaderValue(values));
}
private String GetHeaderValue(IEnumerable<String> cookieHeaders)
{
List<String> currentCookies = new List<String>();
if (cookieHeaders != null)
{
foreach (String value in cookieHeaders)
{
currentCookies.AddRange(value.Split(';').Select(x => x.Trim()));
}
}
currentCookies.RemoveAll(x => String.IsNullOrEmpty(x));
foreach (Cookie cookie in m_cookies)
{
// Remove all existing cookies that match the name of the cookie we are going to add.
currentCookies.RemoveAll(x => String.Equals(x.Substring(0, x.IndexOf('=')), cookie.Name, StringComparison.OrdinalIgnoreCase));
currentCookies.Add(String.Concat(cookie.Name, "=", cookie.Value));
}
return String.Join("; ", currentCookies);
}
private CookieCollection m_cookies;
private static readonly String s_cookieHeader = HttpRequestHeader.Cookie.ToString();
}
}

View File

@@ -1,157 +0,0 @@
using System;
using System.Net;
using System.Net.Http;
using GitHub.Services.Common;
using System.Globalization;
namespace GitHub.Services.Client
{
/// <summary>
/// Provides authentication for internet identities using single-sign-on cookies.
/// </summary>
internal sealed class VssFederatedTokenProvider : IssuedTokenProvider, ISupportSignOut
{
public VssFederatedTokenProvider(
VssFederatedCredential credential,
Uri serverUrl,
Uri signInUrl,
String issuer,
String realm)
: base(credential, serverUrl, signInUrl)
{
Issuer = issuer;
Realm = realm;
}
protected override String AuthenticationScheme
{
get
{
return "TFS-Federated";
}
}
protected override String AuthenticationParameter
{
get
{
if (String.IsNullOrEmpty(this.Issuer) && String.IsNullOrEmpty(this.Realm))
{
return String.Empty;
}
else
{
return String.Format(CultureInfo.InvariantCulture, "issuer=\"{0}\", realm=\"{1}\"", this.Issuer, this.Realm);
}
}
}
/// <summary>
/// Gets the federated credential from which this provider was created.
/// </summary>
public new VssFederatedCredential Credential
{
get
{
return (VssFederatedCredential)base.Credential;
}
}
/// <summary>
/// Gets a value indicating whether or not a call to get token will require interactivity.
/// </summary>
public override Boolean GetTokenIsInteractive
{
get
{
return this.CurrentToken == null;
}
}
/// <summary>
/// Gets the issuer for the token provider.
/// </summary>
public String Issuer
{
get;
private set;
}
/// <summary>
/// Gets the realm for the token provider.
/// </summary>
public String Realm
{
get;
private set;
}
protected internal override Boolean IsAuthenticationChallenge(IHttpResponse webResponse)
{
if (!base.IsAuthenticationChallenge(webResponse))
{
return false;
}
// This means we were proactively constructed without any connection information. In this case
// we return false to ensure that a new provider is reconstructed with all appropriate configuration
// to retrieve a new token.
if (this.SignInUrl == null)
{
return false;
}
String realm, issuer;
VssFederatedCredential.GetRealmAndIssuer(webResponse, out realm, out issuer);
return this.Realm.Equals(realm, StringComparison.OrdinalIgnoreCase) &&
this.Issuer.Equals(issuer, StringComparison.OrdinalIgnoreCase);
}
protected override IssuedToken OnValidatingToken(
IssuedToken token,
IHttpResponse webResponse)
{
// If the response has Set-Cookie headers, attempt to retrieve the FedAuth cookie from the response
// and replace the current token with the new FedAuth cookie. Note that the server only reissues the
// FedAuth cookie if it is issued for more than an hour.
CookieCollection fedAuthCookies = CookieUtility.GetFederatedCookies(webResponse);
if (fedAuthCookies != null)
{
// The reissued token should have the same user information as the previous one.
VssFederatedToken federatedToken = new VssFederatedToken(fedAuthCookies)
{
Properties = token.Properties,
UserId = token.UserId,
UserName = token.UserName
};
token = federatedToken;
}
return token;
}
public void SignOut(Uri signOutUrl, Uri replyToUrl, String identityProvider)
{
// The preferred implementation is to follow the signOutUrl with a browser and kill the browser whenever it
// arrives at the replyToUrl (or if it bombs out somewhere along the way).
// This will work for all Web-based identity providers (Live, Google, Yahoo, Facebook) supported by ACS provided that
// the TFS server has registered sign-out urls (in the TF Registry) for each of these.
// This is the long-term approach that should be pursued and probably the approach recommended to other
// clients which don't have direct access to the cookie store (TEE?)
// In the short term we are simply going to delete the TFS cookies and the Windows Live cookies that are exposed to this
// session. This has the drawback of not properly signing out of Live (you'd still be signed in to e.g. Hotmail, Xbox, MSN, etc.)
// but will allow the user to re-enter their live credentials and sign-in again to TFS.
// The other drawback is that the clients will have to be updated again when we pursue the implementation outlined above.
CookieUtility.DeleteFederatedCookies(replyToUrl);
if (!String.IsNullOrEmpty(identityProvider) && identityProvider.Equals("Windows Live ID", StringComparison.OrdinalIgnoreCase))
{
CookieUtility.DeleteWindowsLiveCookies();
}
}
}
}