/// <remarks> /// For dev.azure.com-style URLs we use the path arg to get the Azure DevOps organization name. /// We ensure the presence of the path arg by setting credential.useHttpPath = true at install time. /// /// The result of this workaround is that we are now unable to determine if the user wanted to store /// credentials with the full path or not for dev.azure.com-style URLs. /// /// Rather than always assume we're storing credentials against the full path, and therefore resulting /// in an personal access token being created per remote URL/repository, we never store against /// the full path and always store with the organization URL "dev.azure.com/org". /// /// For visualstudio.com-style URLs we know the AzDevOps organization name from the host arg, and /// don't set the useHttpPath option. This means if we get the full path for a vs.com-style URL /// we can store against the full remote path (the intended design). /// /// Users that need to clone a repository from Azure Repos against the full path therefore must /// use the vs.com-style remote URL and not the dev.azure.com one. /// </remarks> private static string GetServiceName(InputArguments input) { if (!input.TryGetHostAndPort(out string hostName, out _)) { throw new InvalidOperationException("Failed to parse host name and/or port"); } // dev.azure.com if (UriHelpers.IsDevAzureComHost(hostName)) { // We can never store the new dev.azure.com-style URLs against the full path because // we have forced the useHttpPath option to true to in order to retrieve the AzDevOps // organization name from Git. return(UriHelpers.CreateOrganizationUri(input).AbsoluteUri.TrimEnd('/')); } // *.visualstudio.com if (UriHelpers.IsVisualStudioComHost(hostName)) { // If we're given the full path for an older *.visualstudio.com-style URL then we should // respect that in the service name. return(input.GetRemoteUri().AbsoluteUri.TrimEnd('/')); } throw new InvalidOperationException("Host is not Azure DevOps."); }
public override async Task <ICredential> GenerateCredentialAsync(InputArguments input) { ThrowIfDisposed(); // We should not allow unencrypted communication and should inform the user if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) { throw new Exception("Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS."); } Uri orgUri = UriHelpers.CreateOrganizationUri(input); Uri remoteUri = input.GetRemoteUri(); // Determine the MS authentication authority for this organization Context.Trace.WriteLine("Determining Microsoft Authentication Authority..."); string authAuthority = await _azDevOps.GetAuthorityAsync(orgUri); Context.Trace.WriteLine($"Authority is '{authAuthority}'."); // Get an AAD access token for the Azure DevOps SPS Context.Trace.WriteLine("Getting Azure AD access token..."); JsonWebToken accessToken = await _msAuth.GetAccessTokenAsync( authAuthority, AzureDevOpsConstants.AadClientId, AzureDevOpsConstants.AadRedirectUri, AzureDevOpsConstants.AadResourceId, remoteUri, null); string atUser = accessToken.GetAzureUserName(); Context.Trace.WriteLineSecrets($"Acquired Azure access token. User='******' Token='{{0}}'", new object[] { accessToken.EncodedToken }); // Ask the Azure DevOps instance to create a new PAT var patScopes = new[] { AzureDevOpsConstants.PersonalAccessTokenScopes.ReposWrite, AzureDevOpsConstants.PersonalAccessTokenScopes.ArtifactsRead }; Context.Trace.WriteLine($"Creating Azure DevOps PAT with scopes '{string.Join(", ", patScopes)}'..."); string pat = await _azDevOps.CreatePersonalAccessTokenAsync( orgUri, accessToken, patScopes); Context.Trace.WriteLineSecrets("PAT created. PAT='{0}'", new object[] { pat }); return(new GitCredential(Constants.PersonalAccessTokenUserName, pat)); }
/// <remarks> /// For dev.azure.com-style URLs we use the path arg to get the Azure DevOps organization name. /// We ensure the presence of the path arg by setting credential.useHttpPath = true at install time. /// /// The result of this workaround is that we are now unable to determine if the user wanted to store /// credentials with the full path or not for dev.azure.com-style URLs. /// /// Rather than always assume we're storing credentials against the full path, and therefore resulting /// in an personal access token being created per remote URL/repository, we never store against /// the full path and always store with the organization URL "dev.azure.com/org". /// /// For visualstudio.com-style URLs we know the AzDevOps organization name from the host arg, and /// don't set the useHttpPath option. This means if we get the full path for a vs.com-style URL /// we can store against the full remote path (the intended design). /// /// Users that need to clone a repository from Azure Repos against the full path therefore must /// use the vs.com-style remote URL and not the dev.azure.com one. /// </remarks> private static string GetServiceName(Uri remoteUri) { // dev.azure.com if (UriHelpers.IsDevAzureComHost(remoteUri.Host)) { // We can never store the new dev.azure.com-style URLs against the full path because // we have forced the useHttpPath option to true to in order to retrieve the AzDevOps // organization name from Git. return(UriHelpers.CreateOrganizationUri(remoteUri, out _).AbsoluteUri.TrimEnd('/')); } // *.visualstudio.com if (UriHelpers.IsVisualStudioComHost(remoteUri.Host)) { // If we're given the full path for an older *.visualstudio.com-style URL then we should // respect that in the service name. return(remoteUri.AbsoluteUri.TrimEnd('/')); } throw new InvalidOperationException("Host is not Azure DevOps."); }
public override string GetCredentialKey(InputArguments input) { return($"git:{UriHelpers.CreateOrganizationUri(input).AbsoluteUri}"); }
private async Task <IMicrosoftAuthenticationResult> GetAzureAccessTokenAsync(Uri remoteUri, string userName) { // We should not allow unencrypted communication and should inform the user if (StringComparer.OrdinalIgnoreCase.Equals(remoteUri.Scheme, "http")) { throw new Exception("Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS."); } Uri orgUri = UriHelpers.CreateOrganizationUri(remoteUri, out string orgName); _context.Trace.WriteLine($"Determining Microsoft Authentication authority for Azure DevOps organization '{orgName}'..."); string authAuthority = _authorityCache.GetAuthority(orgName); if (authAuthority is null) { // If there is no cached value we must query for it and cache it for future use _context.Trace.WriteLine($"No cached authority value - querying {orgUri} for authority..."); authAuthority = await _azDevOps.GetAuthorityAsync(orgUri); _authorityCache.UpdateAuthority(orgName, authAuthority); } _context.Trace.WriteLine($"Authority is '{authAuthority}'."); // // If the remote URI is a classic "*.visualstudio.com" host name and we have a user specified from the // remote then take that as the current AAD/MSA user in the first instance. // // For "dev.azure.com" host names we only use the user info part of the remote when this doesn't // match the Azure DevOps organization name. Our friends in Azure DevOps decided "borrow" the username // part of the remote URL to include the organization name (not an actual username). // // If we have no specified user from the remote (or this is [email protected]/org/..) then query the // user manager for a bound user for this organization, if one exists... // var icmp = StringComparer.OrdinalIgnoreCase; if (!string.IsNullOrWhiteSpace(userName) && (UriHelpers.IsVisualStudioComHost(remoteUri.Host) || (UriHelpers.IsAzureDevOpsHost(remoteUri.Host) && !icmp.Equals(orgName, userName)))) { _context.Trace.WriteLine("Using username as specified in remote."); } else { _context.Trace.WriteLine($"Looking up user for organization '{orgName}'..."); userName = _bindingManager.GetUser(orgName); } _context.Trace.WriteLine(string.IsNullOrWhiteSpace(userName) ? "No user found." : $"User is '{userName}'."); // Get an AAD access token for the Azure DevOps SPS _context.Trace.WriteLine("Getting Azure AD access token..."); IMicrosoftAuthenticationResult result = await _msAuth.GetTokenAsync( authAuthority, GetClientId(), GetRedirectUri(), AzureDevOpsConstants.AzureDevOpsDefaultScopes, userName); _context.Trace.WriteLineSecrets( $"Acquired Azure access token. Account='{result.AccountUpn}' Token='{{0}}'", new object[] { result.AccessToken }); return(result); }