Closed goldsam closed 2 months ago
Hi @goldsam, thank you for your feature request!
Could I ask you to clone the branch feat/76
and test the Microsoft Entra ID authentication scheme?
I would be happy to, but I may not have time until this coming weekend.
I've been testing branch feat/76
, and I have some feedback:
I would recommend removing the following two lines: https://github.com/tnc1997/azure-app-configuration-emulator/blob/fa6514cec07d8a0a61eaf9c07cd5a86bc015e090/src/AzureAppConfigurationEmulator/Program.cs#L37-L38
My justification is:
ValidateAudience
and ValidateIssuer
are true
by default.false
should they want to.NOTE: It was not initially obvious to me what the
Audience
andIssuer
should be.
Using the official ConfigurationClient
with Entra Id authentication turned out to be non-trivial. I had solve the following challenges:
ClientSecretCredential
and DefaultAzureCredential
will attempt to use the emulator's base address as the resource principal name passed to Entra Id via the OAuth scope parameter when authenticating. This failed with the following exception message:Azure.Identity.AuthenticationFailedException : ClientSecretCredential authentication failed: AADSTS500011: The resource principal named https://127.0.0.1:56052 was not found in the tenant named [REDACTED]. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant. Trace ID: [REDACTED] Correlation ID: [REDACTED] Timestamp: 2024-06-06 21:50:35Z
My solution was:
Set an Application ID URI for my application registration. I used a URI of the form api://<appId>
.
Create a custom TokenCredential
decorator/wrapper to override the scopes passed when requesting a token. The actual token is acquired through a wrapped/delegate TokenCredential
(such as ClientSecretCredential
):
NOTE: The scope I used successfully was of the form
api://<appId>/.default
.
Below is my TokenCredential
wrapper class:
using Azure.Core;
using Microsoft.Extensions.Configuration;
class ScopesOverridingTokenCredential : TokenCredential
{
private readonly TokenCredential _innerTokenCredential;
private readonly string[] _scopes;
public ScopesOverridingTokenCredential(TokenCredential innerTokenCredential, params string[] scopes)
: this(innerTokenCredential, scopes.AsEnumerable())
{ }
public ScopesOverridingTokenCredential(TokenCredential innerTokenCredential, IEnumerable<string> scopes)
{
_innerTokenCredential = innerTokenCredential;
_scopes = scopes as string[] ?? scopes.ToArray();
}
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) =>
_innerTokenCredential.GetToken(CreateModifiedTokenRequestContext(requestContext), cancellationToken);
public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) =>
await _innerTokenCredential.GetTokenAsync(CreateModifiedTokenRequestContext(requestContext), cancellationToken);
private TokenRequestContext CreateModifiedTokenRequestContext(TokenRequestContext requestContext) =>
new TokenRequestContext(_scopes, requestContext.ParentRequestId, requestContext.Claims, requestContext.TenantId, requestContext.IsCaeEnabled);
}
I would love to find a better way to solve this issue.
ConfigurationClient
refuses to connect to a URI endpoint over an unsecured HTTP connection.
This behavior cannot be overridden since:
ConfigurationClient(Uri endpoint, TokenCredential credential, ConfigurationClientOptions options)
constructor hardcodes the use of a BearerTokenAuthenticationPolicy
.BearerTokenAuthenticationPolicy
hardcodes a check that the target service URL uses the https
URI scheme.Thus, I was forced to deploy the emulator with TLS enabled.
To achieve this, I dynamically provisioned a self-signed certificate which is mounted into the running azure-app-configuration-emulator
container and configured via env-vars.
NOTE: My team uses TestContainers for orchestrating our integration tests (often consisting of several containers) for which have created custom extensions to simplify the setup described above. It might be worth publishing a TestContainer module - a simple lib which would streamline configuring and starting an
azure-app-configuration-emulator
container in the context of complex tests.
Follow the previous points, it would probably by important to provide more extensive guidance and code examples for using your emulator with Microsoft Entra Id.
I would recommend removing the following two lines:
My justification is:
ValidateAudience
andValidateIssuer
aretrue
by default.- Overriding them in this manner prevents a user from setting them to
false
should they want to.NOTE: It was not initially obvious to me what the
Audience
andIssuer
should be.
I would like to retain these two lines because the audience is a static value and the issuer is automatically determined from the discovery document that is based on the MetadataAddress
for the Microsoft Entra tenant. That being said I have moved the ValidAudience
into appsettings.json
to allow it to optionally be overridden to e.g. https://contoso.azconfig.io if desired. It is also worth noting that ValidateAudience
and ValidateIssuer
are set by JwtBearerConfigureOptions
based on the presence of one or more audiences
and one or more issuers
respectively.
- Using the official
ConfigurationClient
with Entra Id authentication turned out to be non-trivial. I had solve the following challenges:
Unfortunately this is one of the issues of using an emulated Azure resource with a genuine Microsoft Entra tenant. The genuine Microsoft Entra tenant will not have any knowledge of the emulated Azure resource. This is likely one of the reasons that the Azure Cosmos DB emulator for example only allows authentication using a key and not Microsoft Entra ID.
I would love to find a better way to solve this issue.
This is not necessarily a better way to solve the issue, but if you are emulating a genuine Azure App Configuration resource, then you could alternatively configure the emulator as if it is the genuine Azure App Configuration resource:
openssl req -x509 -out ./emulator.crt -keyout ./emulator.key -newkey rsa:2048 -nodes -sha256 -subj '/CN=contoso.azconfig.io' -addext 'subjectAltName=DNS:contoso.azconfig.io'
using Azure.Data.AppConfiguration;
using Azure.Identity;
var tenantId = Environment.GetEnvironmentVariable("Authentication__Schemes__MicrosoftEntraId__TenantId")!;
var clientId = Environment.GetEnvironmentVariable("Authentication__Schemes__MicrosoftEntraId__ClientId")!;
var clientSecret = Environment.GetEnvironmentVariable("Authentication__Schemes__MicrosoftEntraId__ClientSecret")!;
var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
var endpoint = Environment.GetEnvironmentVariable("AzureAppConfiguration__Endpoint")!;
var client = new ConfigurationClient(new Uri(endpoint), credential);
var setting = new ConfigurationSetting("AzureAppConfigurationEmulator", "Hello World");
await client.SetConfigurationSettingAsync(setting);
services:
azure-app-configuration-emulator:
build:
context: https://github.com/tnc1997/azure-app-configuration-emulator.git
dockerfile: ./src/AzureAppConfigurationEmulator/Dockerfile
environment:
- ASPNETCORE_HTTP_PORTS=80
- ASPNETCORE_HTTPS_PORTS=443
- Authentication__Schemes__MicrosoftEntraId__MetadataAddress=https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/v2.0/.well-known/openid-configuration
- Authentication__Schemes__MicrosoftEntraId__ValidAudience=https://contoso.azconfig.io
networks:
default:
aliases:
- contoso.azconfig.io
volumes:
- ./emulator.crt:/usr/local/share/azureappconfigurationemulator/emulator.crt:ro
- ./emulator.key:/usr/local/share/azureappconfigurationemulator/emulator.key:ro
console-application:
build: .
depends_on:
- azure-app-configuration-emulator
entrypoint: /bin/sh -c "update-ca-certificates && dotnet ConsoleApplication.dll"
environment:
- Authentication__Schemes__MicrosoftEntraId__ClientId
- Authentication__Schemes__MicrosoftEntraId__ClientSecret
- Authentication__Schemes__MicrosoftEntraId__TenantId=00000000-0000-0000-0000-000000000000
- AzureAppConfiguration__Endpoint=https://contoso.azconfig.io
volumes:
- ./emulator.crt:/usr/local/share/ca-certificates/emulator.crt:ro
I would say this is a lot better! Documenting this strategy would be very helpful to new users.
Thank you for this wisdom, I will be tinkering with this todayπ
As I mentioned before, a lot of this work could even be captured in a Testcontainers library. Any interest in supporting this? If so, it would require that an image is published to a registry somewhere.
I can update you with my feedback on your most recent changes near the end of today (I'm EST time zone). However, with the exception of possible documentation enhancements, this looks pretty sound.
Thank your for addressing this so quickly and engaging with me. Your project is pretty awesome!
I would say this is a lot better!
Thank you very much, it requires genuine resources for the emulator to emulate, but it reduces code complexity.
Documenting this strategy would be very helpful to new users.
Hopefully you agree that the latest update to the Microsoft Entra ID section document this approach well enough.
Any interest in supporting this?
This is definitely something that I will look at in more detail once an image has been published to Docker Hub.
I ran into another snag - Entra still would need to trust https://contoso.azconfig.io
. Is there some way to configure my Entra Id Application registration appropriately?
Entra still would need to trust https://contoso.azconfig.io.
https://contoso.azconfig.io
was just being used as an example endpoint. You would need to create an Azure App Configuration resource, then grant your registered Microsoft Entra application the appropriate role(s) for that resource.
Hi again πAfter a few days of working with this, I am going to stand my ground and reiterate my suggestion to remove those two lines.
While I appreciate your security values, I think it is sufficient to choose safe default settings and allow users to override them should they want to (as I do). In my case, I struggled quite a bit with these in my particular use case where just disabling validation is fine (as my testing runs a completely isolated environment).
As I mentioned before, ValidateAudience
and ValidateIssuer
are true by default. Individuals who tinker are going to be technical chaps who accept the risk. I'm really hoping I can persuade you on this π
Hi @goldsam, thank you very much for getting back to me!
As I mentioned before,
ValidateAudience
andValidateIssuer
are true by default.
I'm not sure if you saw my previous reply on Friday 7th June but this is not the case for the JwtBearerOptions
.
.AddJwtBearer("MicrosoftEntraId", options =>
{
// options.TokenValidationParameters.ValidateAudience = true;
// options.TokenValidationParameters.ValidateIssuer = true;
options.ForwardDefaultSelector = context =>
{
if (AuthenticationHeaderValue.TryParse(context.Request.Headers.Authorization, out var value))
{
if (value.Scheme.Equals("HMAC-SHA256", StringComparison.OrdinalIgnoreCase))
{
return HmacDefaults.AuthenticationScheme;
}
}
return null;
};
});
The metadata address is required and that implicitly provides the issuer to use for token validation whilst the ValidAudience
is overridable depending on the scope of the role that is assigned to the service principal.
if (string.IsNullOrWhiteSpace(validationParameters.ValidIssuer) && validationParameters.ValidIssuers.IsNullOrEmpty<string>() && string.IsNullOrWhiteSpace(configuration?.Issuer))
If possible could I ask which parts of the configuration were difficult so that we might improve the documentation?
I'm sorry - I must have been working way to late when I was last testing. I was able to get things working fine with your most recent commit. You can merge your code and call this one done.
For what it is worth, I was able to avoid the need to create a real app configuration instance as follows:
Using the ScopesOverridingTokenCredential
class above, I override the scope used when authenticating to be https://azconfig.io/.default
(the original scope was https://{docker-network-address-for-emulator}/.default
)
Configure the appropriate Entra metadata address for my organization.
Configure ValidIssuer
as https://sts.windows.net/{tenantid}/
by setting the Authentication__Schemes__MicrosoftEntraId__ValidIssuer
env-var.
BTW: Thank you for your patience on this π
For what it is worth, I was able to avoid the need to create a real app configuration instance as follows:
Yeah a genuine Azure App Configuration resource can be avoided by modifying the role from being resource level scoped https://<endpoint>.azconfig.io/.default
to being subscription level scoped https://azconfig.io/.default
.
BTW: Thank you for your patience on this π
No problem at all, you're welcome, glad that we could get this working π
My organization has many applications which are configured/restricted to use only Azure Entra (Specifically client secret) authentication when accessing any Azure service. We strive to test our applications in as similar an environment as productions as is possible without creating test-only code paths. Thus, it would be beneficial to authenticate with your app configuration emulator either using Entra auth or to be able to disable authentication requirements entirely.