Skip to content

Instantly share code, notes, and snippets.

@KarishmaGhiya
Last active May 5, 2024 10:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KarishmaGhiya/81ee6265ab6e9109d3bf510678878b34 to your computer and use it in GitHub Desktop.
Save KarishmaGhiya/81ee6265ab6e9109d3bf510678878b34 to your computer and use it in GitHub Desktop.
[Identity] API Design for Azure Service connections Credential in Azure Pipelines

Introduction

Introducing a credential support for federated token authorization on Azure Pipelines in Azure SDK for Identity.

As a user, one should be able to use Azure Services from an Azure Pipeline task without using secrets. The Azure Pipelines provides a workload federation identity support for this through ARM Service Connections. This credential is designed to explicitly support this scenario for Azure Pipelines.

image

Read up on details on how federation identity is enabled for Service Connections - https://devblogs.microsoft.com/devops/public-preview-of-workload-identity-federation-for-azure-pipelines/

Why is this credential so important?

  • As part of Spring Grove, the service teams across Microsoft need to stop using the service principals for authentication and need to use the Federated Identity feature in Azure Pipelines. Today they don't have a credential that will work seemlessly for this scenario.
  • Edge service team has created a custom credential, https://microsoft.visualstudio.com/Edge/_git/edgeinternal.es?path=/UtilityLibraries/Microsoft.Edge.ES.Azure.Identity/AzureDevOpsFederatedTokenCredential.cs
  • Other teams are copying over this credential within their source code and reaching out to the Edge team for support.
  • While others are trying to use existing credentials like DefaultAzureCredential or WorkloadIdentityCredential with the assumption that the scenario is supported and getting confused with the error messages.
  • Every other day, there is a new service team reaching out to us for supporting the scenario.
  • It's important to provide a solution with seamless user experience.

Proposing API update

For reference please look at the PR here - Azure/azure-sdk-for-js#29392

  1. For the authentication, the required parameters are - tenantId, clientId and serviceConnectionId that will be passed in through the constructor. All these are corresponding to the Azure Service Connections they want to authenticate.
  2. Then we have an async callback that reads in the required 5 system variables from the Azure Pipeline environment and makes a rest API call to Azure Devops to request an OIDC token.
  3. Pass in this OIDC token to ClientAssertionCredential.

The API changes look like this - image

User sample

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

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

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

Real-time example of using this end to end with the yaml pipeline - https://microsoft.visualstudio.com/Edge/_git/edgeinternal.es?path=/sealion/ci/templates/deployment-template.yml&version=GBmaster&_a=contents and https://microsoft.visualstudio.com/Edge/_git/edgeinternal.es?path=/sealion/ci/deployment.yml&version=GBmaster&_a=contents

Existing workaround

Today the users can use the ClientAssertionCredential and write their own callback for getting the OIDC token, but that is not a great user experience. Given how crucial and widely-used this scenario is becoming, repititive code residing in all of the service team's source codes, is not a great solution.

const credential = new ClientAssertionCredential({
      tenantId,
      clientId,
      clientAssertion: devopsServiceConnectionAssertion("0dec29c2-a766-4121-9c2e-1894f5aca5cb"),
    })
    
 //define the callback devopsServiceConnectionAssertion
 // users will need to handle the complicated logic of this callback themselves

What does Identity SDK need to do?

Looking at the workings of the WI in the diagram above, we see that in order for the WI to work for Service Connections, an OIDC token needs to be provided by Azure Devops first and then the call to get the access token for AAD authentication is made with the OIDC token.

The Azure Devops DOES NOT automatically provide the OIDC token to the environment. It has to be requested with the help of a rest api and the request url for which is formulated with the help of a few system variables that are always available in Devops.

Rest API Call - OIDC token

Look at this REST API call made in Powershell script - https://github.com/geekzter/azure-identity-scripts/blob/e6a4bbc67ffd97433db46f822c96d47b11d02d18/scripts/azure-devops/set_terraform_azurerm_vars.ps1#L43 It makes use of the following system variables to build the OIDC token request url:

To build the authorization header for this rest api call, it uses a secret provided in the devops environment called SYSTEM_ACCESSTOKEN - https://github.com/geekzter/azure-identity-scripts/blob/e6a4bbc67ffd97433db46f822c96d47b11d02d18/scripts/azure-devops/set_terraform_azurerm_vars.ps1#L54

Now let's compare this to what we actually see in our Devops Pipeline. I have enabled system debugging on one of the pipeline runs here - https://dev.azure.com/azure-sdk/internal/_build/results?buildId=3499927&view=logs&j=3dc8fd7e-4368-5a92-293e-d53cefc8c4b3&t=e77055a3-6358-5204-c080-7a2e41553284

image

We see the service connection id variable ENDPOINT_AUTH... and something called as SECRET_SYSTEM_ACCESSTOKEN (instead of SYSTEM_ACCESS_TOKEN) in our pipelines.

The other 4 system variables are available as well: image Notice how the OIDC token is granted and is not available as an env var. So we need to do the same in our SDK.

Now if you look at all the service connections assoicated with this pipeline, and look at the corresponding logs for the task where it downloads the secrets, each service connection (in this case the different keyvaults are our service connections) has a different "ENDPOINT_AUTH_XXX" with it.

Proposal for Rest API call

In order to understand which exact service connection the user needs to authenticate with, we request them to provide the service connection id as a parameter to be certain that we are authenticating to the correct service connections.

Then we can make the rest api call. Snippet from the TS code here -

private async requestOidcToken(oidcRequestUrl: string, systemAccessToken: string): Promise<string> {
    console.log("Requesting OIDC token from Azure DevOps...");
    console.debug(oidcRequestUrl);

    const requestOptions = {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${systemAccessToken}`,
      },
    };

    const response = await fetch(oidcRequestUrl, requestOptions);
    const result = await response.json();
    return result;
  }

Now this Rest API method will provide the OIDC token which can now be used for our step 2.

Requesting Access Token from AAD

Once we have the OIDC token, things are quite simple from here. We can just use this OIDC token like an assertion to request the Access token from Microsoft Entra ID. For this we can simply use ClientAssertionCredential and pass in this OIDC token.

@KarishmaGhiya
Copy link
Author

KarishmaGhiya commented Apr 12, 2024

Please note - We are not supporting this functionality through DAC and do not intend to do so in the future. Because:

  • introduction of complicated design issues and logic in DAC.
  • Niche scenario that can be left out of DAC.
  • Can always make additional changes in the future to DAC with minimal design changes once Devops service provides better environment variable support for this scenario.
  • But if we introduce the support now, we'll have to expose the parameter through DAC Options bag and it can be disruptive to remove in the future.

@KarishmaGhiya
Copy link
Author

Other alternate designs that were considered were to add support to WorkloadIdentity Credential, but it introduced a lot of unnecessary complications like

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment