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.
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
orWorkloadIdentityCredential
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
- For the authentication, the required parameters are -
tenantId
,clientId
andserviceConnectionId
that will be passed in through the constructor. All these are corresponding to the Azure Service Connections they want to authenticate. - 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.
- Pass in this OIDC token to
ClientAssertionCredential
.
The API changes look like this -
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:
- SYSTEM_TEAMFOUNDATIONCOLLECTIONURI
- SYSTEM_TEAMPROJECTID
- SYSTEM_PLANID
- SYSTEM_JOBID
- serviceConnectionId (This is extracted from the variable name of the shape
ENDPOINT_AUTH_d267d7b2-a67e-4f43-8a8a-bdff194d7233
just show like here - https://github.com/geekzter/azure-identity-scripts/blob/e6a4bbc67ffd97433db46f822c96d47b11d02d18/scripts/azure-devops/set_terraform_azurerm_vars.ps1#L37
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
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: 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.