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

Enable the use of Azure Workload Identities with .AddClient(options => { options.UseWebProviders().AddMicrosoft...}) #2065

Open
1 task done
karlschriek opened this issue May 6, 2024 · 3 comments

Comments

@karlschriek
Copy link

Confirm you've already contributed to this project or that you sponsor it

  • I confirm I'm a sponsor or a contributor

Describe the solution you'd like

The current implementation of .AddClient when using Microsoft Entra authentication looks as follows:

                options.UseWebProviders()
                    .AddMicrosoft(options =>
                        {
                            options.SetClientId(Configuration["AzureAd:ClientId"]);
                            options.SetClientSecret(Configuration["AzureAd:ClientCredentials:0:ClientSecret"]);
                            options.SetTenant(Configuration["AzureAd:TenantId"]);
                            options.SetRedirectUri("callback/login/microsoft");
                        }
                    );

The options.SetClientSecret(Configuration["AzureAd:ClientCredentials:0:ClientSecret"]); part is problematic for us since in all of our deployments we strive for credential-free configuration of all Azure-related service. To achieve this we make heavy use for Workload Identities (https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview?tabs=dotnet).

Workload Identities essentially work by configuring the Kubernetes cluster as an OIDC provider and regarding the specific workload (i.e. the Pod on which the client is being served) as an identity on that provider. (This is achieved by injecting an identity token into the pod using a mutating webhook, but that isn't really important here). What is important is that the microsoft identity library is able to exchange that identity token for an access token against the Microsoft Entra application, thereby establishing authenticating client. Normally you would need a client secret to achieve that.

The vanilla builder.Services.AddAuthentication(AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) allows for this by setting the "SourceType" for client credentials to "SignedAssertionFilePath" (see https://github.com/AzureAD/microsoft-identity-web/wiki/v2.0#credentials-are-generalizing-certificates for more context),

Below would be the setup for a Workload Identity

  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "xxxx-xxxx-xxxx-xxxx-xxxxxxxxx"
    "ClientId": "xxxx-xxxx-xxxx-xxxx-xxxxxxxxx",
    "ClientCredentials": [
      {
        "SourceType": "SignedAssertionFilePath" 
      }
    ]
  },

The same setup using an explicit ClientSecret:

"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "TenantId": "xxxx-xxxx-xxxx-xxxx-xxxxxxxxx"
  "ClientId": "xxxx-xxxx-xxxx-xxxx-xxxxxxxxx",
  "ClientCredentials": [
    {
      "ClientSecret": "f-D8Q~xxxxxxxxxxxxxxxxxx",
      "SourceType": "ClientSecret"
    }
  ],
},

We would like to have the ability to differentiate between these two approaches when using an OpenIddict client as well. For example, having a options.SetClientCredentials(Configuration["AzureAd:ClientCredentials"]); setting that will behave differently depending on what has been set in "SourceType".

Additional context

No response

@kevinchalet
Copy link
Member

kevinchalet commented May 6, 2024

What is important is that the microsoft identity library is able to exchange that identity token for an access token against the Microsoft Entra application, thereby establishing authenticating client. Normally you would need a client secret to achieve that.

The thing is we can't - or more exactly don't want to 😄 - depend on the Azure (Identity) SDK, which is where all that sorcery is implemented, so it might not be straightforward to support.

Essentially, workload identities are nothing more than Azure-managed asymmetric keys that are used to generate client assertions - stored in a file referenced by the AZURE_FEDERATED_TOKEN_FILE environment variable - that will be used with the standard private_key_jwt client authentication method (see https://github.com/Azure/azure-sdk-for-net/blob/2869a4b020f8575fec4a6bfb362ba1496961fafe/sdk/identity/Azure.Identity/src/Credentials/WorkloadIdentityCredential.cs).

private_key_jwt client authentication is something the OpenIddict client itself already fully supports but it is not (yet) easily configurable via OpenIddictClientWebIntegrationBuilder.Microsoft, that only offers to register a client secret and not a RSA/ECDSA key or a X.509 certificate.

Exposing a new builder method to allow specifying a RSA/ECDSA signing key or a X.509 signing certificate should be easy, but anything more involved will require more work: not a strong "no", but if it's not sponsored work, we'll need a stronger demand to justify spending time on that 😃

@karlschriek
Copy link
Author

The only real motivation I can offer is that Workload Identities (as they are known on Azure and GCP) and IAM Roles for Service Accounts (as they are known on AWS, but which are basically the same thing) are by far the most secure way to do identity access management on Kubernetes-based workloads. The fact that we never need to generate, store, rotate or mount client secrets essentially eliminates all the external interaction points where those credentials can leak. You still have to keep the Pod itself and the Webhook that does the mutating locked down, but that is a much more manageable problem.

As to the magic that the identity sdk does, you are right that it is just working with client assertions. The Kubernetes cluster becomes a federated OIDC provider to an Azure AD Application, and the service account (identitfied as system:serviceaccount:<namespace>:<service-account>) is the identity about which the assertion is made. You are obviously much more versed in how this works, but I can imagine that you could piggy-back on a more generic private_key_jwt auth mechanism to make something like this work.

The webhook periodically refreshes the token at AZURE_FEDERATED_TOKEN_FILE, so as long as it is re-read with every authentication attempt you also don't need to worry about managing refreshes.

By the way here is an example of the token located at AZURE_FEDERATED_TOKEN_FILE

{
  "alg": "RS256",
  "kid": "asdfsa32344sadvcxbxbsdfg435r"
}
{
  "aud": [
    "api://AzureADTokenExchange"
  ],
  "exp": 1715103340,
  "iat": 1715016940,
  "iss": "https://xxx.oic.prod-aks.azure.com/xxxx-xxx-xxx-xxx-xxxxxx/xxx-xxx-xxx-xxx-xxx/",
  "kubernetes.io": {
    "namespace": "mynamespace",
    "pod": {
      "name": "mypod-86d45b9cd9-bglrc",
      "uid": "xxxx-xxx-yyy-yyyyy-yyyyyyy"
    },
    "serviceaccount": {
      "name": "openiddict-server",
      "uid": "yyyyyy-ccc-vvvv-yyyyy-xxxxxx"
    }
  },
  "nbf": 1715016940,
  "sub": "system:serviceaccount:mynamespace:openiddict-server"
}

@kevinchalet
Copy link
Member

kevinchalet commented May 6, 2024

The only real motivation I can offer is that Workload Identities (as they are known on Azure and GCP) and IAM Roles for Service Accounts (as they are known on AWS, but which are basically the same thing) are by far the most secure way to do identity access management on Kubernetes-based workloads. The fact that we never need to generate, store, rotate or mount client secrets essentially eliminates all the external interaction points where those credentials can leak. You still have to keep the Pod itself and the Webhook that does the mutating locked down, but that is a much more manageable problem.

I'm not denying "managed identities" are a good thing from both a security and usability perspective, but they are proprietary/provider-specific (the client authentication method is standard, but how the token is exposed by the host and retrieved is 100% implementation-specific): Microsoft can afford implementing that in their Azure Identity library because they own the whole chain, but OpenIddict is not tied to Azure and is meant to be usable everywhere, so such an Azure-specific feature would objectively have a limited audience 😃

As I said, I'm not fundamentally opposed to supporting such features, but there must be a stronger demand for them (or a company willing to fund the work needed to implement that and maintain it long-term).

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

No branches or pull requests

2 participants