Skip to content

Add Workload Identity Federation as a credential type on feed endpoints#707

Open
Rikocher wants to merge 1 commit into
microsoft:masterfrom
Rikocher:feat/workload-identity-federation
Open

Add Workload Identity Federation as a credential type on feed endpoints#707
Rikocher wants to merge 1 commit into
microsoft:masterfrom
Rikocher:feat/workload-identity-federation

Conversation

@Rikocher

Copy link
Copy Markdown

Summary

Adds Workload Identity Federation as a third credential type on the existing ARTIFACTS_CREDENTIALPROVIDER_FEED_ENDPOINTS per-feed JSON configuration, alongside the certificate-based Service Principal and Managed Identity flows already supported. Users add a clientAssertionFilePath to a feed entry; a new MsalFederatedIdentityTokenProvider exchanges the signed JWT client assertion for an Entra access token via MSAL''s confidential-client WithClientAssertion flow.

Motivation

Today there is no supported path for using the credential provider with a federated-credential-configured App Registration. The nearest existing provider, MsalServicePrincipalTokenProvider, requires an X.509 client certificate on TokenRequest.ClientCertificate and has no field for a client assertion string. Callers who authenticate via WIF (GitHub Actions OIDC, Azure DevOps federated service connections, AKS workload identity pods, Azure VMs where a user-assigned managed identity signs assertions for an App Registration, etc.) currently have to fall back to:

  • Manually calling az account get-access-token and stuffing the result into NuGet.Config as a bearer -- brittle, expires in ~1 hour, and misses the credprovider''s caching / renewal.
  • Long-lived Personal Access Tokens -- the exact anti-pattern WIF is meant to eliminate.

Design

Follows the existing architecture exactly: WIF is another credential type on the same JSON blob that already carries SP-cert and MI configuration, wired through the same VstsBuildTaskServiceEndpointCredentialProvider and VstsBuildTaskMsalTokenProvidersFactory. Symmetric with clientCertificateFilePath.

JSON schema addition (all fields optional):

{
  "endpointCredentials": [{
    "endpoint": "https://pkgs.dev.azure.com/.../nuget/v3/index.json",
    "clientId": "<App Registration client id>",
    "tenantId": "<tenant hosting the App Reg>",
    "clientAssertionFilePath": "/var/run/secrets/azure/tokens/azure-identity-token"
  }]
}

What this PR does NOT change

  • Existing providers (Silent, WIA, Broker, Interactive, DeviceCode, Service Principal, Managed Identity, Build Task, external endpoints) are untouched.
  • VstsCredentialProvider (interactive flow) is untouched.
  • Pre-existing behavior: absent clientAssertionFilePath, the SP-cert / MI providers run exactly as before.
  • The pre-existing ARTIFACTS_CREDENTIALPROVIDER_RETURN_ENTRA_TOKENS env var is not modified -- only its use is documented as the recommended companion setting.

Changes

File Change
src/Authentication/TokenRequest.cs Add nullable ClientAssertionFilePath property
src/Authentication/MsalFederatedIdentityTokenProvider.cs New -- confidential-client MSAL provider that reads the assertion file on every call
CredentialProvider.Microsoft/Util/FeedEndpointCredentialsParser.cs Add tenantId and clientAssertionFilePath optional fields on EndpointCredentials
.../VstsBuildTaskServiceEndpointCredentialProvider.cs Populate TokenRequest.ClientAssertionFilePath from JSON; prefer JSON tenantId over discovered authority tenant
.../VstsBuildTaskMsalTokenProvidersFactory.cs Yield MsalFederatedIdentityTokenProvider before SP / MI
README.md Extend the ARTIFACTS_CREDENTIALPROVIDER_FEED_ENDPOINTS schema doc with the new fields
src/Authentication.Tests/TokenProviderTests.cs New MsalFederatedIdentityContractTest mirroring MsalServicePrincipalContractTest

141 insertions, 4 deletions across 7 files.

Behavior

  • MsalFederatedIdentityTokenProvider.CanGetToken() returns true only when all of ClientId, TenantId, ClientAssertionFilePath are set on the TokenRequest AND the assertion file exists on disk.
  • The assertion file is re-read on every token acquisition (via WithClientAssertion(Func<string>)), so external refreshers (kubelet, scheduled tasks, Azure Pipelines OIDC re-signing) take effect without restarting the caller.
  • Ordering: federated -> SP-cert -> MI. First provider whose CanGetToken returns true wins.

Companion setting

Because WIF authenticates as a service principal -- and SPNs cannot mint Azure DevOps VssSessionTokens (ADO returns 403 on POST /_apis/Token/SessionTokens) -- users on the WIF path should also set the existing ARTIFACTS_CREDENTIALPROVIDER_RETURN_ENTRA_TOKENS=true opt-in so the provider returns the Entra bearer directly as basic-auth password. That env var pre-exists this PR; the README extension documents the pairing.

Testing

  • Added MsalFederatedIdentityContractTest covering the CanGetToken matrix: all set + file exists -> true; any of ClientId/TenantId/ClientAssertionFilePath missing -> false; path set but file missing -> false.
  • Full auth test suite: 17 passed, 0 failed on net8.0 (7 skipped are network-gated integration tests unrelated to this change).
  • Manually validated end-to-end on an Azure VM: a user-assigned managed identity signs JWT assertions for an App Registration that is a Feed Reader on an Azure DevOps Artifacts feed. dotnet restore and nuget install both succeed with no PAT and no interactive prompt.

Related links

Follow-ups (not in this PR)

  • Could add a small env-var -> JSON adapter that recognizes the standard Azure SDK AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_FEDERATED_TOKEN_FILE contract and synthesizes an EndpointCredentials entry -- happy to do that in a follow-up if maintainers want it.

Happy to iterate on naming, ordering, or split into smaller commits if that helps review.

Adds federated-identity authentication alongside the existing certificate-based Service Principal and Managed Identity flows on the ARTIFACTS_CREDENTIALPROVIDER_FEED_ENDPOINTS JSON configuration. Users add a per-feed 'clientAssertionFilePath' pointing at a file containing a signed JWT client assertion; MsalFederatedIdentityTokenProvider exchanges it for an Entra access token via MSAL's confidential-client WithClientAssertion flow.

The nearest existing provider, MsalServicePrincipalTokenProvider, requires an X.509 client certificate on TokenRequest.ClientCertificate and has no field for a client assertion string. This closes the gap for callers configured with Workload Identity Federation (GitHub Actions OIDC, Azure DevOps federated service connections, AKS workload identity, Azure VMs where a user-assigned managed identity signs assertions for an App Registration, etc.).

Configuration is purely additive: three new optional JSON fields (tenantId, clientAssertionFilePath) mirror the shape of the existing certificate fields. When clientAssertionFilePath is absent, behavior is unchanged and the existing SP/MI providers run exactly as before.

The assertion file is re-read on every token acquisition via WithClientAssertion(Func<string>), so external refreshers (kubelet, scheduled tasks, Azure Pipelines OIDC re-signing) take effect without restarting the caller.

Recommended companion: set ARTIFACTS_CREDENTIALPROVIDER_RETURN_ENTRA_TOKENS=true so the Entra bearer is returned directly (service principals cannot mint Azure DevOps session tokens). That env var pre-exists this PR.

Signed-off-by: Richard Kochert <rikocher@microsoft.com>
@Rikocher

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="Microsoft"

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant