From 78546132d4e1669567dff64b6f81168bbffa2f5e Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Tue, 11 Jun 2024 09:18:12 -0500 Subject: [PATCH] AzurePipelinesCredential uses new environment variables (#44297) --- sdk/identity/Azure.Identity/CHANGELOG.md | 14 ++++ .../Azure.Identity/TROUBLESHOOTING.md | 11 +++ .../api/Azure.Identity.netstandard2.0.cs | 2 +- .../samples/OtherCredentialSamples.md | 12 +-- .../Azure.Identity/samples/TokenCache.md | 1 + .../Credentials/AzurePipelinesCredential.cs | 51 ++++++++----- .../AzurePipelinesCredentialOptions.cs | 29 +------- .../AzurePipelinesCredentialLiveTests.cs | 48 ++++++++++++ .../tests/AzurePipelinesCredentialTests.cs | 74 ++++++++++++------- .../tests/samples/OtherCredentialSnippets.cs | 10 +-- sdk/identity/test-resources-post.ps1 | 2 +- sdk/identity/test-resources-pre.ps1 | 2 +- sdk/identity/tests.yml | 18 +++++ 13 files changed, 189 insertions(+), 85 deletions(-) create mode 100644 sdk/identity/Azure.Identity/tests/AzurePipelinesCredentialLiveTests.cs diff --git a/sdk/identity/Azure.Identity/CHANGELOG.md b/sdk/identity/Azure.Identity/CHANGELOG.md index 69618776f1f4..a4532289bc13 100644 --- a/sdk/identity/Azure.Identity/CHANGELOG.md +++ b/sdk/identity/Azure.Identity/CHANGELOG.md @@ -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://github.com/Azure/azure-sdk-for-net/issues/43796) + ## 1.11.4 (2024-06-10) ### Bugs Fixed diff --git a/sdk/identity/Azure.Identity/TROUBLESHOOTING.md b/sdk/identity/Azure.Identity/TROUBLESHOOTING.md index c7108b0b5834..c6e4e873ff14 100644 --- a/sdk/identity/Azure.Identity/TROUBLESHOOTING.md +++ b/sdk/identity/Azure.Identity/TROUBLESHOOTING.md @@ -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 '' 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 | 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":"","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://github.com/Azure/azure-sdk-for-net/blob/main/SUPPORT.md) at the root of the repo. diff --git a/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs b/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs index 6d4a5793da14..63ab76663a86 100644 --- a/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs +++ b/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs @@ -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 GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken) { throw null; } } diff --git a/sdk/identity/Azure.Identity/samples/OtherCredentialSamples.md b/sdk/identity/Azure.Identity/samples/OtherCredentialSamples.md index 6827ea0f413d..d726935566df 100644 --- a/sdk/identity/Azure.Identity/samples/OtherCredentialSamples.md +++ b/sdk/identity/Azure.Identity/samples/OtherCredentialSamples.md @@ -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 = ""; -string tenantId = ""; -string serviceConnectionId = ""; +// Replace the following values with the actual values from the details for your service connection. +string clientId = ""; +string tenantId = ""; +string serviceConnectionId = ""; // 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); diff --git a/sdk/identity/Azure.Identity/samples/TokenCache.md b/sdk/identity/Azure.Identity/samples/TokenCache.md index 477eb7e7102f..8d13311638ac 100644 --- a/sdk/identity/Azure.Identity/samples/TokenCache.md +++ b/sdk/identity/Azure.Identity/samples/TokenCache.md @@ -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 | diff --git a/sdk/identity/Azure.Identity/src/Credentials/AzurePipelinesCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/AzurePipelinesCredential.cs index e9b6d5d1556b..e0d11dabe50b 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/AzurePipelinesCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/AzurePipelinesCredential.cs @@ -16,9 +16,11 @@ namespace Azure.Identity /// 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; } @@ -35,27 +37,32 @@ protected AzurePipelinesCredential() /// /// The tenant ID for the service connection. /// The client ID for the service connection. - /// The service connection ID, as found in the querystring's resourceId key. + /// The service connection Id for the service connection associated with the pipeline. + /// The pipeline's System.AccessToken value. /// An instance of . - /// When , , or is null. - public AzurePipelinesCredential(string tenantId, string clientId, string serviceConnectionId, AzurePipelinesCredentialOptions options = default) + /// When is null. + 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> _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); } @@ -82,25 +89,22 @@ internal async ValueTask 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; } @@ -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; } } } diff --git a/sdk/identity/Azure.Identity/src/Credentials/AzurePipelinesCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/AzurePipelinesCredentialOptions.cs index 90a466dfce37..1cd2507a694b 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/AzurePipelinesCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/AzurePipelinesCredentialOptions.cs @@ -16,34 +16,9 @@ public class AzurePipelinesCredentialOptions : TokenCredentialOptions, ISupports internal MsalConfidentialClient MsalClient { get; set; } /// - /// The security token used by the running build. + /// The URI of the OIDC request endpoint. /// - internal string SystemAccessToken { get; set; } = Environment.GetEnvironmentVariable("SYSTEM_ACCESSTOKEN"); - - /// - /// The URI of the TFS collection or Azure DevOps organization. - /// - internal string CollectionUri { get; set; } = Environment.GetEnvironmentVariable("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI"); - - /// - /// A unique identifier for a single attempt of a single job. The value is unique to the current pipeline. - /// - internal string JobId { get; set; } = Environment.GetEnvironmentVariable("SYSTEM_JOBID"); - - /// - /// A string-based identifier for a single pipeline run. - /// - internal string PlanId { get; set; } = Environment.GetEnvironmentVariable("SYSTEM_PLANID"); - - /// - /// The ID of the project that this build belongs to. - /// - internal string TeamProjectId { get; set; } = Environment.GetEnvironmentVariable("SYSTEM_TEAMPROJECTID"); - - /// - /// The hub under which this pipeline is running - typically "build" or "release". - /// - internal string HubName { get; set; } = Environment.GetEnvironmentVariable("SYSTEM_HOSTTYPE"); + internal string OidcRequestUri { get; set; } = Environment.GetEnvironmentVariable("SYSTEM_OIDCREQUESTURI"); /// public IList AdditionallyAllowedTenants { get; internal set; } = new List(); diff --git a/sdk/identity/Azure.Identity/tests/AzurePipelinesCredentialLiveTests.cs b/sdk/identity/Azure.Identity/tests/AzurePipelinesCredentialLiveTests.cs new file mode 100644 index 000000000000..19eb2a93a4e2 --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/AzurePipelinesCredentialLiveTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core; +using Azure.Core.TestFramework; +using NUnit.Framework; + +namespace Azure.Identity.Tests +{ + public class AzurePipelinesCredentialLiveTests : IdentityRecordedTestBase + { + public AzurePipelinesCredentialLiveTests(bool isAsync) : base(isAsync) + { } + + [Test] + [LiveOnly] + public async Task AzurePipelineCredentialLiveTest_GetToken() + { + var systemAccessToken = Environment.GetEnvironmentVariable("SYSTEM_ACCESSTOKEN"); + var tenantId = Environment.GetEnvironmentVariable("AZURE_SERVICE_CONNECTION_TENANT_ID"); + var clientId = Environment.GetEnvironmentVariable("AZURE_SERVICE_CONNECTION_CLIENT_ID"); + var serviceConnectionId = Environment.GetEnvironmentVariable("AZURE_SERVICE_CONNECTION_ID"); + + if (string.IsNullOrEmpty(systemAccessToken) || string.IsNullOrEmpty(tenantId) || string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(serviceConnectionId)) + { + var envVars = Environment.GetEnvironmentVariables(); + StringBuilder sb = new StringBuilder(); + foreach (var key in envVars.Keys) + { + sb.AppendLine($"{key}: {envVars[key]}"); + } + Console.WriteLine(sb); + Assert.Fail($"{sb} SYSTEM_ACCESSTOKEN: {systemAccessToken}, AZURE_SERVICE_CONNECTION_TENANT_ID: {tenantId}, AZURE_SERVICE_CONNECTION_CLIENT_ID: {clientId}, AZURE_SERVICE_CONNECTION_ID: {serviceConnectionId}"); + Assert.Ignore("AzurePipelinesCredentialLiveTests disabled because required environment variables are not set"); + } + + var cred = new AzurePipelinesCredential(tenantId, clientId, serviceConnectionId, systemAccessToken); + + AccessToken token = await cred.GetTokenAsync(new TokenRequestContext(new[] { "https://management.azure.com//.default" }), CancellationToken.None); + + Assert.IsNotNull(token.Token); + } + } +} diff --git a/sdk/identity/Azure.Identity/tests/AzurePipelinesCredentialTests.cs b/sdk/identity/Azure.Identity/tests/AzurePipelinesCredentialTests.cs index 2c38e3daa795..abe67976d96a 100644 --- a/sdk/identity/Azure.Identity/tests/AzurePipelinesCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/AzurePipelinesCredentialTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Text; using System.Threading; using System.Threading.Tasks; using Azure.Core; @@ -11,6 +12,7 @@ namespace Azure.Identity.Tests { + [NonParallelizable] public class AzurePipelinesCredentialTests : CredentialTestBase { public AzurePipelinesCredentialTests(bool isAsync) : base(isAsync) @@ -18,9 +20,14 @@ public AzurePipelinesCredentialTests(bool isAsync) : base(isAsync) public override TokenCredential GetTokenCredential(TokenCredentialOptions options) { - var clientAssertionOptions = new AzurePipelinesCredentialOptions { Diagnostics = { IsAccountIdentifierLoggingEnabled = options.Diagnostics.IsAccountIdentifierLoggingEnabled }, MsalClient = mockConfidentialMsalClient, Pipeline = CredentialPipeline.GetInstance(null) }; + var pipelineOptions = new AzurePipelinesCredentialOptions + { + Diagnostics = { IsAccountIdentifierLoggingEnabled = options.Diagnostics.IsAccountIdentifierLoggingEnabled }, + MsalClient = mockConfidentialMsalClient, + Pipeline = CredentialPipeline.GetInstance(null), + }; - return InstrumentClient(new AzurePipelinesCredential(expectedTenantId, ClientId, "serviceConnectionId", clientAssertionOptions)); + return InstrumentClient(new AzurePipelinesCredential(TenantId, ClientId, Guid.NewGuid().ToString(), "mytoken", options: pipelineOptions)); } public override TokenCredential GetTokenCredential(CommonCredentialTestConfig config) @@ -36,12 +43,7 @@ public override TokenCredential GetTokenCredential(CommonCredentialTestConfig co AdditionallyAllowedTenants = config.AdditionallyAllowedTenants, IsUnsafeSupportLoggingEnabled = config.IsUnsafeSupportLoggingEnabled, MsalClient = config.MockConfidentialMsalClient, - CollectionUri = "https://dev.azure.com/myorg/myproject/_apis/serviceendpoint/endpoints?api-version=2.2.2", - PlanId = "myplan", - JobId = "myjob", - TeamProjectId = "myteamproject", - SystemAccessToken = "mytoken", - HubName = "myhub", + OidcRequestUri = "https://dev.azure.com/myorg/myproject/_apis/serviceendpoint/endpoints?api-version=2.2.2", }; if (config.Transport != null) { @@ -53,17 +55,17 @@ public override TokenCredential GetTokenCredential(CommonCredentialTestConfig co } config.TransportConfig.ResponseHandler = (req, resp) => { - if (options.CollectionUri.Contains(req.Uri.Host)) + if (options.OidcRequestUri.Contains(req.Uri.Host)) { Assert.That(req.Headers.TryGetValue("Authorization", out var authHeader), Is.True); - Assert.That(authHeader, Does.Contain(options.SystemAccessToken)); + Assert.That(authHeader, Does.Contain("mytoken")); resp.SetContent("""{"oidcToken": "myoidcToken"}"""); } }; var pipeline = CredentialPipeline.GetInstance(options); options.Pipeline = pipeline; - return InstrumentClient(new AzurePipelinesCredential(config.TenantId, ClientId, "serviceConnectionId", options)); + return InstrumentClient(new AzurePipelinesCredential(config.TenantId, ClientId, "myConnectionId", "mytoken", options: options)); } [Test] @@ -71,31 +73,51 @@ public void AzurePipelinesCredentialOptions_Loads_From_Env() { using (new TestEnvVar(new Dictionary { - { "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", "mockCollectionUri" }, - { "SYSTEM_HOSTTYPE", "mockHubName" }, - { "SYSTEM_JOBID", "mockJobId" }, - { "SYSTEM_PLANID", "mockPlanId" }, - { "SYSTEM_ACCESSTOKEN", "mockSystemAccessToken" }, - { "SYSTEM_TEAMPROJECTID", "mockTeamProjectId" }})) + { "SYSTEM_OIDCREQUESTURI", "mockCollectionUri" }, + })) { var options = new AzurePipelinesCredentialOptions(); - Assert.AreEqual("mockCollectionUri", options.CollectionUri); - Assert.AreEqual("mockJobId", options.JobId); - Assert.AreEqual("mockPlanId", options.PlanId); - Assert.AreEqual("mockSystemAccessToken", options.SystemAccessToken); - Assert.AreEqual("mockTeamProjectId", options.TeamProjectId); - Assert.AreEqual("mockHubName", options.HubName); + Assert.AreEqual("mockCollectionUri", options.OidcRequestUri); } } [Test] public async Task AzurePipelineCredentialWorksInChainedCredential() { - var chainedCred = new ChainedTokenCredential(new AzurePipelinesCredential("mockTenantID", "mockClientId", "serviceConnectionId"), new MockCredential()); + using (new TestEnvVar(new Dictionary + { + { "SYSTEM_OIDCREQUESTURI", null }, + })) + { + var chainedCred = new ChainedTokenCredential(new AzurePipelinesCredential("myTenantId", "myClientId", "myConnectionId", "mytoken"), new MockCredential()); - AccessToken token = await chainedCred.GetTokenAsync(new TokenRequestContext(new[] { "scope" }), CancellationToken.None); + AccessToken token = await chainedCred.GetTokenAsync(new TokenRequestContext(new[] { "scope" }), CancellationToken.None); - Assert.AreEqual("mockToken", token.Token); + Assert.AreEqual("mockToken", token.Token); + } + } + + [Test] + public void AzurePipelineCredentialReturnsErrorInformation() + { + using (new TestEnvVar(new Dictionary + { + { "SYSTEM_OIDCREQUESTURI", "mockCollectionUri" }, + })) + { + var systemAccessToken = "mytoken"; + var tenantId = "myTenantId"; + var clientId = "myClientId"; + var serviceConnectionId = "myConnectionId"; + + var mockTransport = new MockTransport(req => new MockResponse(200).WithContent( + $"{{\"token_type\": \"Bearer\",\"expires_in\": 9999,\"ext_expires_in\": 9999,\"access_token\": \"mytoken\" }}")); + + var options = new AzurePipelinesCredentialOptions { Transport = mockTransport }; + var cred = new AzurePipelinesCredential(tenantId, clientId, serviceConnectionId, systemAccessToken, options); + + Assert.ThrowsAsync(async () => await cred.GetTokenAsync(new TokenRequestContext(new[] { "scope" }), CancellationToken.None)); + } } public class MockCredential : TokenCredential diff --git a/sdk/identity/Azure.Identity/tests/samples/OtherCredentialSnippets.cs b/sdk/identity/Azure.Identity/tests/samples/OtherCredentialSnippets.cs index 25f42cc76b61..f74d8cc0b960 100644 --- a/sdk/identity/Azure.Identity/tests/samples/OtherCredentialSnippets.cs +++ b/sdk/identity/Azure.Identity/tests/samples/OtherCredentialSnippets.cs @@ -15,13 +15,13 @@ public class OtherCredentialSnippets public void AzurePipelinesCredential_Example() { #region Snippet:AzurePipelinesCredential_Example - // Replace the following values with the actual values for the service connection. - string clientId = ""; - string tenantId = ""; - string serviceConnectionId = ""; + // Replace the following values with the actual values from the details for your service connection. + string clientId = ""; + string tenantId = ""; + string serviceConnectionId = ""; // 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); diff --git a/sdk/identity/test-resources-post.ps1 b/sdk/identity/test-resources-post.ps1 index 6e7ee9ee04dd..e7bb5f88a5d1 100644 --- a/sdk/identity/test-resources-post.ps1 +++ b/sdk/identity/test-resources-post.ps1 @@ -9,7 +9,7 @@ $workingFolder = $webappRoot; if ($null -ne $Env:AGENT_WORKFOLDER) { $workingFolder = $Env:AGENT_WORKFOLDER } -az login --service-principal -u $DeploymentOutputs['IDENTITY_CLIENT_ID'] -p $DeploymentOutputs['IDENTITY_CLIENT_SECRET'] --tenant $DeploymentOutputs['IDENTITY_TENANT_ID'] + az account set --subscription $DeploymentOutputs['IDENTITY_SUBSCRIPTION_ID'] # Deploy the webapp diff --git a/sdk/identity/test-resources-pre.ps1 b/sdk/identity/test-resources-pre.ps1 index 5940d7b9ef38..08e5300a45bc 100644 --- a/sdk/identity/test-resources-pre.ps1 +++ b/sdk/identity/test-resources-pre.ps1 @@ -24,7 +24,7 @@ $sshKey = Get-Content $PSScriptRoot/sshKey.pub $templateFileParameters['sshPubKey'] = $sshKey # Get the max version that is not preview and then get the name of the patch version with the max value -az login --service-principal -u $TestApplicationId -p $TestApplicationSecret --tenant $TenantId +az login --service-principal -u $TestApplicationId --tenant $TenantId --allow-no-subscriptions --federated-token $env:ARM_OIDC_TOKEN $versions = az aks get-versions -l westus -o json | ConvertFrom-Json Write-Host "AKS versions: $($versions | ConvertTo-Json -Depth 100)" $patchVersions = $versions.values | Where-Object { $_.isPreview -eq $null } | Select-Object -ExpandProperty patchVersions diff --git a/sdk/identity/tests.yml b/sdk/identity/tests.yml index 988bc49c598b..d730fc591ac9 100644 --- a/sdk/identity/tests.yml +++ b/sdk/identity/tests.yml @@ -3,6 +3,20 @@ trigger: none extends: template: /eng/pipelines/templates/stages/archetype-sdk-tests.yml parameters: + PreSteps: + - task: AzureCLI@2 + displayName: Set OIDC variables + env: + ARM_OIDC_TOKEN: $(ARM_OIDC_TOKEN) + inputs: + azureSubscription: azure-sdk-tests + scriptType: pscore + scriptLocation: inlineScript + addSpnToEnvironment: true + inlineScript: | + Write-Host "##vso[task.setvariable variable=ARM_CLIENT_ID;issecret=true]$($env:servicePrincipalId)" + Write-Host "##vso[task.setvariable variable=ARM_TENANT_ID;issecret=true]$($env:tenantId)" + Write-Host "##vso[task.setvariable variable=ARM_OIDC_TOKEN;issecret=true]$($env:idToken)" TimeoutInMinutes: 120 AdditionalMatrixConfigs: - Name: identity_msi @@ -16,3 +30,7 @@ extends: - $(sub-config-azure-cloud-test-resources) # Contains alternate tenant, AAD app and cert info for testing - $(sub-config-identity-test-resources) + ServiceConnection: azure-sdk-tests + EnvVars: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + UseFederatedAuth: true \ No newline at end of file