{
  "@context": [
    "https://www.w3.org/ns/credentials/v2"
  ],
  "type": [
    "VerifiableCredential",
    "BlogPostCredential"
  ],
  "id": "urn:uuid:f7e02f40-c248-4c78-9ee9-1552c5ea6671",
  "issuer": "did:webvh:QmTVQnV3qGxWzWmnmWJAy1zkYswgbUmE95K5qodmAizVfr:mjendza.net",
  "validFrom": "2026-03-15T13:35:32Z",
  "credentialSubject": {
    "title": "Entra ID Workload Identity Federation: Secure Workloads Without Secrets (with Terraform Demo)",
    "author": "Mateusz Jendza",
    "body": "![enum](/images/workload-federation/fed.jpg)\r\n\r\n## TL;DR\r\n- Use my demo OpenID Connect provider to test workload identity federation in Entra ID.\r\n- Use my Terraform module and example to create an Azure AD application with federated identity credentials.\r\n- Play with workload identity federation without the need for secrets.\r\n- Integrate your workloads with external identity providers like GitHub or Kubernetes.\r\n\r\n## Introduction\r\nNo more secrets! It is 2025, and our identity operations should be more secure and easier to manage. Microsoft Entra ID Workload Identity Federation enables you to utilise external identity providers (such as GitHub, Workload on Kubernetes cluster, SPIFFE, or SPIRE) to authenticate workloads without requiring secrets. In this post, I’ll guide you through setting up federated identity credentials in Entra ID using a custom OIDC provider and Terraform.\r\n\r\n## Big Picture\r\n\r\n![workload-identity-federation](/images/workload-federation/diagram.jpg)\r\n\r\n## MS Documentation\r\n- Basic: https://learn.microsoft.com/en-us/entra/workload-id/workload-identities-overview\r\n- https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation\r\n- token exchange: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#third-case-access-token-request-with-a-federated-credential\r\n\r\n## Token provider\r\nInstead of using GitHub as an example, to make it simple and easy to test and make the demo, I created a simple OpenID Connect provider with the following endpoints:\r\n```http\r\nGET https://api.demo.factorlabs.pl/.well-known/openid-configuration\r\n```\r\nExample response:\r\n```json\r\n{\r\n  \"issuer\": \"https://api.demo.factorlabs.pl\",\r\n  \"jwks_uri\": \"https://api.demo.factorlabs.pl/.well-known/keys\",\r\n  \"id_token_signing_alg_values_supported\": [\r\n    \"RS256\"\r\n  ]\r\n}\r\n```\r\nAnd of course, the key's endpoint, to validate the JWT (Json Web Token) signature - and prove that the token is issued by the provider:\r\n```http\r\nGET https://api.demo.factorlabs.pl/.well-known/keys\r\n```\r\nExample response:\r\n```json\r\n{\r\n  \"keys\": [\r\n    {\r\n      \"kid\": \"526F177AE1470D5B73C202B085997D41EA99A8BE\",\r\n      \"nbf\": 1749577799,\r\n      \"use\": \"sig\",\r\n      \"kty\": \"RSA\",\r\n      \"alg\": \"RS256\",\r\n      \"x5c\": [\r\n        \"MIIFEDCCAs...\"\r\n      ],\r\n      \"x5t\": \"Um8XeuFHDVtzwgKwhZl9QeqZqL4\",\r\n      \"n\": \"pXD23T...\",\r\n      \"e\": \"AQAB\"\r\n    }\r\n  ]\r\n}\r\n```\r\nMy demo **token** provider (endpoint). The endpoint generates a JWT token with a fixed subject and audience. With the Entra ID token endpoint, you can exchange the token for an access token to call the Microsoft Graph API or other API protected by Entra ID.\r\n![enum](/images/workload-federation/mati.jpg)\r\n\r\n```http\r\nPOST https://api.demo.factorlabs.pl/api/workload/token\r\n```\r\n\r\nExample token (JWT encoded) provided by the demo OpenID Connect provider in the point above:\r\n\r\n```json\r\n{\r\n  \"aud\": \"api://AzureADTokenExchange\",\r\n  \"iss\": \"https://api.demo.factorlabs.pl\",\r\n  \"exp\": 1749582266,\r\n  \"iat\": 1749581651,\r\n  \"sub\": \"system:serviceaccount:default:play-with-workload-identity\"\r\n}\r\n```\r\n\r\n## Terraform code\r\nThe example below creates an Azure AD application with federated identity credentials, allowing workload identity federation without the need for secrets. It uses the `azuread` provider to manage Azure Active Directory resources.\r\n\r\n### Terraform Module\r\n``` hcl\r\nvariable \"graph_permissions\" {\r\n    description = \"List of Graph API permissions\"\r\n    type        = list(string)\r\n    default     = []\r\n}\r\nvariable \"business_name\" {\r\n    description = \"Business name\"\r\n    type        = string\r\n}\r\nvariable deployment_env_name {\r\n  description = \"Unique name for the deployment\"\r\n  type        = string\r\n  default     = \"Workshop\"\r\n}\r\nvariable \"enable_workload_identity\" {\r\n    description = \"Enable workload identity federation\"\r\n    type        = bool\r\n}\r\nvariable \"subject_identifier\" {\r\n    description = \"Subject identifier for the federated credential\"\r\n    type        = string\r\n}\r\nvariable \"issuer_url\" {\r\n    description = \"Issuer URL for the federated credential\"\r\n    type        = string\r\n}\r\n\r\nresource \"azuread_application\" \"this\" {\r\n  display_name     = \"TF.${var.deployment_env_name}.${var.business_name}.ServicePrincipal\"\r\n  sign_in_audience = \"AzureADMyOrg\"\r\n  api {\r\n    mapped_claims_enabled          = true\r\n    requested_access_token_version = 2\r\n  }\r\n  feature_tags {\r\n    enterprise = true\r\n    gallery    = true\r\n  }\r\n  required_resource_access {\r\n    # Microsoft Graph\r\n    resource_app_id = \"00000003-0000-0000-c000-000000000000\"\r\n    dynamic \"resource_access\" {\r\n      for_each = var.graph_permissions\r\n      content {\r\n        id   = resource_access.value\r\n        type = \"Role\"\r\n      }\r\n    }\r\n  }\r\n}\r\n\r\nresource \"azuread_service_principal\" \"this\" {\r\n  client_id                    = azuread_application.this.client_id\r\n  app_role_assignment_required = false\r\n}\r\n\r\nresource \"azuread_application_federated_identity_credential\" \"this\" {\r\n  count              = var.enable_workload_identity ? 1 : 0\r\n  application_id     = azuread_application.this.id\r\n  display_name       = \"TF.${var.deployment_env_name}.${var.business_name}.FederatedCredential\"\r\n  description        = \"Workload Identity federation for ${var.business_name}\"\r\n  audiences          = [\"api://AzureADTokenExchange\"]\r\n  issuer             = \"${var.issuer_url}\"\r\n  subject            = var.subject_identifier\r\n}\r\n\r\noutput \"application_id\" {\r\n  value = azuread_application.this.id\r\n}\r\n\r\noutput \"application_client_id\" {\r\n  value = azuread_application.this.client_id\r\n}\r\n\r\noutput \"service_principal_id\" {\r\n  value = azuread_service_principal.this.id\r\n}\r\n\r\n\r\n```\r\n\r\n### Example Usage\r\n```hcl\r\nmodule \"Demo_WorkloadIdentity_ServicePrincipal\" {\r\n  source = \"./modules/service_principal_workload_identity\"\r\n  business_name = \"${var.deployment_unique_name}-WorkloadIdentity\"\r\n  enable_workload_identity = true\r\n  subject_identifier = \"system:serviceaccount:default:play-with-workload-identity\"\r\n  issuer_url = \"https://api.demo.factorlabs.pl\"\r\n  graph_permissions = [\r\n    #application.read.all\r\n    \"PASTE_YOUR_GRAPH_PERMISSION_GUID_HERE\"\r\n    ]\r\n}\r\n```\r\n\r\n## Get Entra ID Token \r\nWhen we deploy the above Terraform code, we can get the token for the service principal using the following HTTP request. \r\nPlease remember to provide your token, tenantId and client ID for service principal! \r\n\r\nFeel free to use my token provider and integrate with your Entra ID to test it.\r\n\r\n^ Do not use my demo token provider with your production environments!\r\n\r\n```http request\r\nPOST https://login.microsoftonline.com/{{tenantId}}/oauth2/v2.0/token\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nclient_id={{clientId}}&\r\nclient_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&\r\nclient_assertion={{subjectToken}}&\r\ngrant_type=client_credentials&\r\nscope=https://graph.microsoft.com/.default\r\n```\r\n\r\n### Summary\r\n- JWT token subject (sub) is the same as the subject in the azuread_application_federated_identity_credential resource.\r\n- JWT token audience (aud) is set to \"api://AzureADTokenExchange\" by default based on the [REST API Documentation](https://learn.microsoft.com/en-us/graph/api/application-post-federatedidentitycredentials?view=graph-rest-1.0&tabs=http#request-body).\r\n- JWT issuer (iss) must be the host for `.well-known` endpoint, the key must be available at `<issuer_url>/.well-known/keys`.\r\n\r\n### Side Note\r\n- [REST Client for VS Code](https://marketplace.visualstudio.com/items/?itemName=humao.rest-client)",
    "datePublished": "2025-06-11",
    "url": "/post/workload-identity-federation",
    "description": "Easy way not to use secrets. Use federated identity credentials in Entra ID.",
    "tags": [
      "Entra-Id",
      "IAM"
    ]
  },
  "proof": {
    "type": "DataIntegrityProof",
    "cryptosuite": "eddsa-jcs-2022",
    "verificationMethod": "did:key:z6MksoqpqENZmzzA4nhCPkfcbWtRHVegGV38Yqu2arRc5Er2#z6MksoqpqENZmzzA4nhCPkfcbWtRHVegGV38Yqu2arRc5Er2",
    "created": "2026-03-15T13:35:32Z",
    "proofPurpose": "assertionMethod",
    "proofValue": "z4JJaPR1WLedQUfNoToqJ7XrHpoXqLiLbgUwdbp1hFtwaEBJ6vmKgeC7mcgbgFjGQ2aMzdyB8VsketzCg3pUdBUuB"
  }
}