Skip to content

Commit

Permalink
AzurePipelinesCredential uses new environment variables (#44297)
Browse files Browse the repository at this point in the history
  • Loading branch information
christothes authored Jun 11, 2024
1 parent 7188acb commit 7854613
Show file tree
Hide file tree
Showing 13 changed files with 189 additions and 85 deletions.
14 changes: 14 additions & 0 deletions sdk/identity/Azure.Identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,23 @@
- All credentials now support setting RefreshOn value if received from MSAL.
- ManagedIdentityCredential sets RefreshOn value of half the token lifetime for AccessTokens with an ExpiresOn value greater than 2 hours in the future.

### Breaking Changes
- The constructor of `AzurePipelinesCredential` now includes additional required parameters for the Azure Pipelines service connection.

### Bugs Fixed
- Bug fixes for `AzurePipelinesCredential`
- Managed identity bug fixes.

## 1.11.4 (2024-06-10)

### Bugs Fixed
- Managed identity bug fixes.

## 1.11.3 (2024-05-07)

### Bugs Fixed
- Fixed a regression in `DefaultAzureCredential` probe request behavior for IMDS managed identity environments. [#43796](https:/Azure/azure-sdk-for-net/issues/43796)

## 1.11.4 (2024-06-10)

### Bugs Fixed
Expand Down
11 changes: 11 additions & 0 deletions sdk/identity/Azure.Identity/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,17 @@ You may also log in another MSA account by selecting "Microsoft account":

![Microsoft account](./images/MSA4.png)

## Troubleshoot AzurePipelinesCredential authentication issues

| Error Message | Description | Mitigation |
| --- | --- | --- |
| AADSTS900023: Specified tenant identifier '<some tenant ID>' is neither a valid DNS name, nor a valid external domain. | The tenant ID passed to the credential is invalid. | Verify the tenant ID is valid. If the service connection was configured via a user-assigned managed identity, the tenant will be the one in which managed identity was registered. If the service connection is configured via a service principal, the tenant should be the one in which the Service Principal is registered. |
| No service connection found with identifier <GUID> | The service connection ID provided is incorrect. | Verify the serviceConnectionId provided. This parameter refers to the `resourceId` of the Azure Service Connection. It can also be found in the query string of the respective Service Connection's configuration page in Azure DevOps. More information about service connections can be found [here](https://learn.microsoft.com/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml) |
| AzurePipelinesCredential: Authentication Failed. oidcToken field not detected in the response. Response = Object moved to here. Status Code: 302. | The system access token seems to be malformed when passing in as a parameter to the credential. | `System.AccessToken` is a required system variable in the Azure Pipelines task and should be provided in the pipeline task, [as mentioned in the docs](https://learn.microsoft.com/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#systemaccesstoken). Verify that the system access token value provided is the predefined variable in Azure Pipelines and isn't malformed. |
| AzurePipelinesCredential: Authentication Failed. oidcToken field not detected in the response. Response = {"$id":"1","innerException":null,"message":"<ACTUAL ERROR MESSAGE>","typeName":"Microsoft.VisualStudio.Services.WebApi.VssInvalidPreviewVersionException, Microsoft.VisualStudio.Services.WebApi","typeKey":"VssInvalidPreviewVersionException","errorCode":0} | When the OIDC token request fails, the OIDC token api throws an error. More details about the specific error are specified in the "message" field of the Response as shown above. | Mitigation will usually depend on the scenario based on what [error message](https://learn.microsoft.com/azure/devops/pipelines/release/troubleshoot-workload-identity?view=azure-devops#error-messages) is being thrown. Make sure you use the [recommended Azure Pipelines task](https://learn.microsoft.com/azure/devops/pipelines/release/troubleshoot-workload-identity?view=azure-devops#review-pipeline-tasks). |
| CredentialUnavailableError: AzurePipelinesCredential is not available: Ensure that you're running this task in an Azure Pipeline so that following missing system variable(s) can be defined: SYSTEM_OIDCREQUESTURI is not set. | This code is not running inside of the Azure Pipelines Environment. You may be running this code locally or on some other environment. | This credential is only designed to run from inside the Azure Pipelines environment for the federated identity to work. |
| AuthenticationRequiredError: unauthorized_client: 700016 - AADSTS700016: Application with identifier 'clientId' was not found in the directory 'Microsoft'. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You may have sent your authentication request to the wrong tenant.| The `clientId` provided is invalid. | Verify the client ID argument is valid. If the service connection's federated identity was registered via a user-assigned managed identity, the client ID of the managed identity should be provided. If the service connection's federated identity is registered via a Service Principal, the Application (client) ID from your app registration should be provided. |

## Get additional help

Additional information on ways to reach out for support can be found in the [SUPPORT.md](https:/Azure/azure-sdk-for-net/blob/main/SUPPORT.md) at the root of the repo.
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public AzureDeveloperCliCredentialOptions() { }
public partial class AzurePipelinesCredential : Azure.Core.TokenCredential
{
protected AzurePipelinesCredential() { }
public AzurePipelinesCredential(string tenantId, string clientId, string serviceConnectionId, Azure.Identity.AzurePipelinesCredentialOptions options = null) { }
public AzurePipelinesCredential(string tenantId, string clientId, string serviceConnectionId, string systemAccessToken, Azure.Identity.AzurePipelinesCredentialOptions options = null) { }
public override Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken) { throw null; }
public override System.Threading.Tasks.ValueTask<Azure.Core.AccessToken> GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken) { throw null; }
}
Expand Down
12 changes: 7 additions & 5 deletions sdk/identity/Azure.Identity/samples/OtherCredentialSamples.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

This example demonstrates authenticating the Key Vault `SecretClient` using the `AzurePipelinesCredential` in an Azure Pipelines environment with [service connections](https://learn.microsoft.com/azure/devops/pipelines/library/service-endpoints).

In the below sample, it is recommended to assign the value of [$(System.AccessToken)](https://learn.microsoft.com/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#systemaccesstoken) to a secure variable in the Azure Pipelines environment.

```C# Snippet:AzurePipelinesCredential_Example
// Replace the following values with the actual values for the service connection.
string clientId = "<service_connection_client_id>";
string tenantId = "<service_connection_tenant_id>";
string serviceConnectionId = "<service_connection_id>";
// Replace the following values with the actual values from the details for your service connection.
string clientId = "<the value of ClientId for the service connections>";
string tenantId = "<the value of TenantId for the service connections>";
string serviceConnectionId = "<the value of service connection Id>";

// Construct the credential.
var credential = new AzurePipelinesCredential(tenantId, clientId, serviceConnectionId);
var credential = new AzurePipelinesCredential(tenantId, clientId, serviceConnectionId, Environment.GetEnvironmentVariable("SYSTEM_ACCESSTOKEN"));

// Use the credential to authenticate with the Key Vault client.
var client = new SecretClient(new Uri("https://keyvault-name.vault.azure.net/"), credential);
Expand Down
1 change: 1 addition & 0 deletions sdk/identity/Azure.Identity/samples/TokenCache.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ The following table indicates the state of in-memory and persistent caching in e
| `AuthorizationCodeCredential` | Supported | Supported |
| `AzureCliCredential` | Not Supported | Not Supported |
| `AzureDeveloperCliCredential` | Not Supported | Not Supported |
| `AzurePipelinesCredential` | Supported | Supported |
| `AzurePowershellCredential` | Not Supported | Not Supported |
| `ClientAssertionCredential` | Supported | Supported |
| `ClientCertificateCredential` | Supported | Supported |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ namespace Azure.Identity
/// </summary>
public class AzurePipelinesCredential : TokenCredential
{
private const string Troubleshooting = "See the troubleshooting guide for more information. https://aka.ms/azsdk/net/identity/azurepipelinescredential/troubleshoot";
internal readonly string[] AdditionallyAllowedTenantIds;
internal string SystemAccessToken { get; }
internal string TenantId { get; }
internal string ClientId { get; }
internal string ServiceConnectionId { get; }
internal MsalConfidentialClient Client { get; }
internal CredentialPipeline Pipeline { get; }
internal TenantIdResolverBase TenantIdResolver { get; }
Expand All @@ -35,27 +37,32 @@ protected AzurePipelinesCredential()
/// </summary>
/// <param name="tenantId">The tenant ID for the service connection.</param>
/// <param name="clientId">The client ID for the service connection.</param>
/// <param name="serviceConnectionId">The service connection ID, as found in the querystring's resourceId key.</param>
/// <param name="serviceConnectionId">The service connection Id for the service connection associated with the pipeline.</param>
/// <param name="systemAccessToken">The pipeline's <see href="https://learn.microsoft.com/azure/devops/pipelines/build/variables?view=azure-devops%26tabs=yaml#systemaccesstoken">System.AccessToken</see> value.</param>
/// <param name="options">An instance of <see cref="AzurePipelinesCredentialOptions"/>.</param>
/// <exception cref="System.ArgumentNullException">When <paramref name="tenantId"/>, <paramref name="clientId"/>, or <paramref name="serviceConnectionId"/> is null.</exception>
public AzurePipelinesCredential(string tenantId, string clientId, string serviceConnectionId, AzurePipelinesCredentialOptions options = default)
/// <exception cref="ArgumentNullException">When <paramref name="systemAccessToken"/> is null.</exception>
public AzurePipelinesCredential(string tenantId, string clientId, string serviceConnectionId, string systemAccessToken, AzurePipelinesCredentialOptions options = default)
{
Argument.AssertNotNull(serviceConnectionId, nameof(serviceConnectionId));
Argument.AssertNotNull(systemAccessToken, nameof(systemAccessToken));
Argument.AssertNotNull(clientId, nameof(clientId));
Argument.AssertNotNull(tenantId, nameof(tenantId));
Argument.AssertNotNull(serviceConnectionId, nameof(serviceConnectionId));

SystemAccessToken = systemAccessToken;
ServiceConnectionId = serviceConnectionId;

options ??= new AzurePipelinesCredentialOptions();
TenantId = Validations.ValidateTenantId(tenantId, nameof(tenantId));
ClientId = clientId;
Pipeline = options?.Pipeline ?? CredentialPipeline.GetInstance(options);
Pipeline = options.Pipeline ?? CredentialPipeline.GetInstance(options);

Func<CancellationToken, Task<string>> _assertionCallback = async (cancellationToken) =>
{
var message = CreateOidcRequestMessage(serviceConnectionId, options ?? new AzurePipelinesCredentialOptions());
var message = CreateOidcRequestMessage(options ?? new AzurePipelinesCredentialOptions());
await Pipeline.HttpPipeline.SendAsync(message, cancellationToken).ConfigureAwait(false);
return GetOidcTokenResponse(message);
};

Client = options?.MsalClient ?? new MsalConfidentialClient(Pipeline, tenantId, clientId, _assertionCallback, options);
Client = options?.MsalClient ?? new MsalConfidentialClient(Pipeline, TenantId, clientId, _assertionCallback, options);
TenantIdResolver = options?.TenantIdResolver ?? TenantIdResolverBase.Default;
AdditionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds((options as ISupportsAdditionallyAllowedTenants)?.AdditionallyAllowedTenants);
}
Expand All @@ -82,25 +89,22 @@ internal async ValueTask<AccessToken> GetTokenCoreAsync(bool async, TokenRequest
}
catch (Exception e)
{
throw scope.FailWrapAndThrow(e);
throw scope.FailWrapAndThrow(e, Troubleshooting);
}
}

internal HttpMessage CreateOidcRequestMessage(string serviceConnectionId, AzurePipelinesCredentialOptions options)
internal HttpMessage CreateOidcRequestMessage(AzurePipelinesCredentialOptions options)
{
string CollectionUri = options.CollectionUri ?? throw new CredentialUnavailableException("AzurePipelinesCredential is not available: environment variable SYSTEM_TEAMFOUNDATIONCOLLECTIONURI is not set.");
string projectId = options.TeamProjectId ?? throw new CredentialUnavailableException("AzurePipelinesCredential is not available: environment variable SYSTEM_TEAMPROJECTID is not set.");
string planId = options.PlanId ?? throw new CredentialUnavailableException("AzurePipelinesCredential is not available: environment variable SYSTEM_PLANID is not set.");
string jobId = options.JobId ?? throw new CredentialUnavailableException("AzurePipelinesCredential is not available: environment variable SYSTEM_JOBID is not set.");
string systemToken = options.SystemAccessToken ?? throw new CredentialUnavailableException("AzurePipelinesCredential is not available: environment variable SYSTEM_ACCESSTOKEN is not set.");
string hubName = options.HubName ?? throw new CredentialUnavailableException("AzurePipelineCredential is not available: environment variable SYSTEM_HOSTTYPE is not set.");
string oidcRequestUri = options.OidcRequestUri ?? throw new CredentialUnavailableException("AzurePipelinesCredential is not available: Ensure that you're running this task in an Azure Pipeline so that following missing system variable(s) can be defined: SYSTEM_OIDCREQUESTURI is not set.");
string systemToken = SystemAccessToken;

var message = Pipeline.HttpPipeline.CreateMessage();

var requestUri = new Uri($"{CollectionUri}{projectId}/_apis/distributedtask/hubs/{hubName}/plans/{planId}/jobs/{jobId}/oidctoken?api-version={OIDC_API_VERSION}&serviceConnectionId={serviceConnectionId}");
var requestUri = new Uri($"{oidcRequestUri}?api-version={OIDC_API_VERSION}&serviceConnectionId={ServiceConnectionId}");
message.Request.Uri.Reset(requestUri);
message.Request.Headers.SetValue(HttpHeader.Names.Authorization, $"Bearer {systemToken}");
message.Request.Headers.SetValue(HttpHeader.Names.ContentType, "application/json");
message.Request.Method = RequestMethod.Post;
return message;
}

Expand All @@ -121,7 +125,16 @@ internal string GetOidcTokenResponse(HttpMessage message)
}
}
}
return oidcToken ?? throw new AuthenticationFailedException("OIDC token not found in response.");
if (oidcToken is null)
{
string error = $"OIDC token not found in response. " + Troubleshooting;
if (message.Response.Status != 200)
{
error = error + $"\n\nResponse= {message.Response.Content}";
}
throw new AuthenticationFailedException(error);
}
return oidcToken;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,9 @@ public class AzurePipelinesCredentialOptions : TokenCredentialOptions, ISupports
internal MsalConfidentialClient MsalClient { get; set; }

/// <summary>
/// The security token used by the running build.
/// The URI of the OIDC request endpoint.
/// </summary>
internal string SystemAccessToken { get; set; } = Environment.GetEnvironmentVariable("SYSTEM_ACCESSTOKEN");

/// <summary>
/// The URI of the TFS collection or Azure DevOps organization.
/// </summary>
internal string CollectionUri { get; set; } = Environment.GetEnvironmentVariable("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI");

/// <summary>
/// A unique identifier for a single attempt of a single job. The value is unique to the current pipeline.
/// </summary>
internal string JobId { get; set; } = Environment.GetEnvironmentVariable("SYSTEM_JOBID");

/// <summary>
/// A string-based identifier for a single pipeline run.
/// </summary>
internal string PlanId { get; set; } = Environment.GetEnvironmentVariable("SYSTEM_PLANID");

/// <summary>
/// The ID of the project that this build belongs to.
/// </summary>
internal string TeamProjectId { get; set; } = Environment.GetEnvironmentVariable("SYSTEM_TEAMPROJECTID");

/// <summary>
/// The hub under which this pipeline is running - typically "build" or "release".
/// </summary>
internal string HubName { get; set; } = Environment.GetEnvironmentVariable("SYSTEM_HOSTTYPE");
internal string OidcRequestUri { get; set; } = Environment.GetEnvironmentVariable("SYSTEM_OIDCREQUESTURI");

/// <inheritdoc/>
public IList<string> AdditionallyAllowedTenants { get; internal set; } = new List<string>();
Expand Down
Loading

0 comments on commit 7854613

Please sign in to comment.