Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Identity] Testing service connections on pipeline #29451

Merged
merged 13 commits into from Apr 30, 2024
22 changes: 12 additions & 10 deletions sdk/identity/identity/samples/AzureIdentityExamples.md
Expand Up @@ -119,11 +119,12 @@ To learn more, read [Application and service principal objects in Microsoft Entr

If your application is hosted in Azure, you can make use of [Managed Identity](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview) for hassle free authentication in your production environments.

| Credential with example | Usage |
| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [WorkloadIdentityCredential](#authenticating-in-azure-with-workload-identity) | Authenticate in Azure Kubernetes environment with [Microsoft Entra Workload ID](https://learn.microsoft.com/entra/workload-id/workload-identities-overview), which [integrates with the Kubernetes native capabilities](https://learn.microsoft.com/azure/aks/workload-identity-overview) to federate with any external identity providers. |
| [ManagedIdentityCredential](#authenticating-in-azure-with-managed-identity) | Authenticate in a virtual machine, App Service, Functions app, Cloud Shell, or AKS environment on Azure, with system-assigned managed identity, user-assigned managed identity, or app registration (when working with AKS pod identity). |
| [DefaultAzureCredential](#authenticating-with-defaultazurecredential) | Tries `EnvironmentCredential`, `ManagedIdentityCredential`, `AzureCliCredential`, `AzurePowerShellCredential`, and other credentials sequentially until one of them succeeds. Use this to have your application authenticate using developer tools, service principals or managed identity based on what is available in the current environment without changing your code. |
| Credential with example | Usage |
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [WorkloadIdentityCredential](#authenticating-in-azure-with-workload-identity) | Authenticate in Azure Kubernetes environment with [Microsoft Entra Workload ID](https://learn.microsoft.com/entra/workload-id/workload-identities-overview), which [integrates with the Kubernetes native capabilities](https://learn.microsoft.com/azure/aks/workload-identity-overview) to federate with any external identity providers. |
| [AzurePipelinesServiceConnectionsCredential](#authenticating-in-azure-pipelines-with-service-connections) | Authenticate in Azure Devops environment with [Microsoft Entra Workload ID](https://learn.microsoft.com/entra/workload-id/workload-identities-overview), which [uses Azure Resource Manager service connections](https://learn.microsoft.com/azure/devops/pipelines/library/connect-to-azure?view=azure-devops#create-an-azure-resource-manager-service-connection-that-uses-workload-identity-federation) to federate with any external identity providers. |
| [ManagedIdentityCredential](#authenticating-in-azure-with-managed-identity) | Authenticate in a virtual machine, App Service, Functions app, Cloud Shell, or AKS environment on Azure, with system-assigned managed identity, user-assigned managed identity, or app registration (when working with AKS pod identity). |
| [DefaultAzureCredential](#authenticating-with-defaultazurecredential) | Tries `EnvironmentCredential`, `ManagedIdentityCredential`, `AzureCliCredential`, `AzurePowerShellCredential`, and other credentials sequentially until one of them succeeds. Use this to have your application authenticate using developer tools, service principals or managed identity based on what is available in the current environment without changing your code. |

### Examples

Expand Down Expand Up @@ -617,19 +618,20 @@ function withUserManagedIdentityCredential() {

This example demonstrates authenticating the `SecretClient` from the [@azure/keyvault-secrets][secrets_client_library] using the `AzurePipelinesServiceConnectionCredential` in an Azure Pipelines environment with service connections.

````ts
```ts
/**
* Authenticate with AzurePipelinesServiceConnection identity.
*/
function withAzurePipelinesServiceConnectionCredential() {
const clientId = "<YOUR_CLIENT_ID>"
const tenantId = "<YOUR_TENANT_ID>"
const serviceConnectionId = "<YOUR_SERVICE_CONNECTION_ID>"
const clientId = "<YOUR_CLIENT_ID>";
const tenantId = "<YOUR_TENANT_ID>";
const serviceConnectionId = "<YOUR_SERVICE_CONNECTION_ID>";
const credential = new AzurePipelinesServiceConnection(tenantId, clientId, serviceConnectionId);

const client = new SecretClient("https://key-vault-name.vault.azure.net", credential);
}
```

This credential is NOT part of `DefaultAzureCredential`.

## Chaining credentials
Expand All @@ -644,7 +646,7 @@ function withChainedTokenCredential() {
);
const client = new SecretClient("https://key-vault-name.vault.azure.net", credential);
}
````
```

## Authenticating With Azure Stack using Azure Identity

Expand Down
Expand Up @@ -14,16 +14,15 @@ import {
import { AzurePipelinesServiceConnectionCredentialOptions } from "./azurePipelinesServiceConnectionCredentialOptions";

const credentialName = "AzurePipelinesServiceConnectionCredential";
const OIDC_API_VERSION = "7.1";
const logger = credentialLogger(credentialName);
const OIDC_API_VERSION = "7.1-preview.1";

/**
* This credential is designed to be used in ADO Pipelines with service connections
* as a setup for workload identity federation.
*/
export class AzurePipelinesServiceConnectionCredential implements TokenCredential {
private clientAssertionCredential: ClientAssertionCredential | undefined;
private serviceConnectionId: string | undefined;

/**
* AzurePipelinesServiceConnectionCredential supports Federated Identity on Azure Pipelines through Service Connections.
Expand Down Expand Up @@ -51,7 +50,7 @@ export class AzurePipelinesServiceConnectionCredential implements TokenCredentia

if (clientId && tenantId && serviceConnectionId) {
this.ensurePipelinesSystemVars();
const oidcRequestUrl = `${process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${process.env.SYSTEM_TEAMPROJECTID}/_apis/distributedtask/hubs/build/plans/${process.env.SYSTEM_PLANID}/jobs/${process.env.SYSTEM_JOBID}/oidctoken?api-version=${OIDC_API_VERSION}&serviceConnectionId=${this.serviceConnectionId}`;
const oidcRequestUrl = `${process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${process.env.SYSTEM_TEAMPROJECTID}/_apis/distributedtask/hubs/build/plans/${process.env.SYSTEM_PLANID}/jobs/${process.env.SYSTEM_JOBID}/oidctoken?api-version=${OIDC_API_VERSION}&serviceConnectionId=${serviceConnectionId}`;
const systemAccessToken = `${process.env.SYSTEM_ACCESSTOKEN}`;
logger.info(
`Invoking ClientAssertionCredential with tenant ID: ${tenantId}, clientId: ${clientId} and service connection id: ${serviceConnectionId}`,
Expand All @@ -78,8 +77,7 @@ export class AzurePipelinesServiceConnectionCredential implements TokenCredentia
options?: GetTokenOptions,
): Promise<AccessToken> {
if (!this.clientAssertionCredential) {
const errorMessage = `${credentialName}: is unavailable. tenantId, clientId, and serviceConnectionId are required parameters.
To use Federation Identity in Azure Pipelines, these are required as inputs / env variables -
const errorMessage = `${credentialName}: is unavailable. To use Federation Identity in Azure Pipelines, these are required as input parameters / env variables -
tenantId,
clientId,
serviceConnectionId,
Expand Down Expand Up @@ -123,17 +121,27 @@ export class AzurePipelinesServiceConnectionCredential implements TokenCredentia
const response = await httpClient.sendRequest(request);
const text = response.bodyAsText;
if (!text) {
throw new AuthenticationError(
response.status,
`${credentialName}: Authenticated Failed. Received null token from OIDC request.`,
logger.error(
`${credentialName}: Authenticated Failed. Received null token from OIDC request. Response status- ${
response.status
}. Complete response - ${JSON.stringify(response)}`,
);
throw new CredentialUnavailableError(
`${credentialName}: Authenticated Failed. Received null token from OIDC request. Response status- ${
response.status
}. Complete response - ${JSON.stringify(response)}`,
);
}
const result = JSON.parse(text);
if (result?.oidcToken) {
return result.oidcToken;
} else {
throw new AuthenticationError(
response.status,
logger.error(
`${credentialName}: Authentication Failed. oidcToken field not detected in the response. Response = ${JSON.stringify(
result,
)}`,
);
throw new CredentialUnavailableError(
`${credentialName}: Authentication Failed. oidcToken field not detected in the response. Response = ${JSON.stringify(
result,
)}`,
Expand Down
Expand Up @@ -2,12 +2,11 @@
// Licensed under the MIT license.

import { AzurePipelinesServiceConnectionCredential } from "../../../src";
import { env, isLiveMode } from "@azure-tools/test-recorder";
import { isLiveMode } from "@azure-tools/test-recorder";
import { assert } from "@azure-tools/test-utils";

describe("AzurePipelinesServiceConnectionCredential", function () {
const scope = "https://vault.azure.net/.default";
const tenantId = env.IDENTITY_SP_TENANT_ID || env.AZURE_TENANT_ID!;
const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when we record these tests, you'd want any env. calls to be made after the recorder is initialized (so like in the test body or a beforeEach block after creating the recorder. Just FYI. I could be wrong but I remember this being a problem for me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. Though with this particular test, it's difficult to have recordings, since I cannot run locally :(

// const clientId = env.IDENTITY_SP_CLIENT_ID || env.AZURE_CLIENT_ID!;

it("authenticates with a valid service connection", async function () {
Expand Down