Skip to content

Instantly share code, notes, and snippets.

@dasiths
Last active June 20, 2021 08:24
Show Gist options
  • Save dasiths/80a5a8b56c1bb33dcb940d8a3ae39f37 to your computer and use it in GitHub Desktop.
Save dasiths/80a5a8b56c1bb33dcb940d8a3ae39f37 to your computer and use it in GitHub Desktop.
TokenCredential based on IConfiguration for .Azure Managed Identity in .NET
/*
More about Azure.Identity client library: https://devblogs.microsoft.com/azure-sdk/azure-identity-august-2020-ga/
Guidance on migrating from old Microsoft.Azure.Services.AppAuthentication library: https://docs.microsoft.com/en-us/dotnet/api/overview/azure/app-auth-migration
This is useful when the user account running the application locally in Visual Studio doesn't have permissions to the resources (i.e. Due to network restrictions/vpn or policy requirements).
We create a SP/AppRegistration and use that identity when running things locally.
TokenCredential is the base class for all credential types used for identity in Azure
like DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, ClientSecretCredential, VisualStudioCrdential etc (https://devblogs.microsoft.com/azure-sdk/azure-identity-august-2020-ga/#more-credential-types)
For almost all scenarios the DefaultAzureCredential will do the trick as it is a composite of many things and will fallback to VisualStudioCrdential when running locally.
But in this example we see how to construct a ClientSecretCredential manually because our VisualStudioCrdential is doesn't have access to the Azure resource we are trying to consume.
In my case it was because I couldn't login as a domain user due to Intune company policy (Consulting for an enterprise client) because my PC was non-compliant.
I could only access Azure from a Jump Box (WVD) but it didn't have enough grunt to run Visual Studio.
This specific constraint meant I had to use the following approach to emulate identity as a service principal when running locally.
*/
public interface ITokenCredentialFactory
{
TokenCredential GetTokenCredential();
}
public class TokenCredentialFactory : ITokenCredentialFactory
{
private readonly IWebHostEnvironment hostingEnvironment;
private readonly IConfiguration configuration;
public TokenCredentialFactory(IWebHostEnvironment hostingEnvironment, IConfiguration configuration)
{
this.hostingEnvironment = hostingEnvironment;
this.configuration = configuration;
}
public TokenCredential GetTokenCredential()
{
bool isCredentialsAvailableInIConfiguration = true; // Check if config has a switch to enable this. We can enable it in secrets.json
if (hostingEnvironment.IsDevelopment() && isCredentialsAvailableInIConfiguration)
{
// get client id, teanant id and secret from IConfiguration here. So the developers can embed them in secrets.json in local env.
return new ClientSecretCredential("tenant id from config", "client if from config", "shared srecret or path to PEM key"); // See https://devblogs.microsoft.com/azure-sdk/azure-identity-august-2020-ga/#authenticating-service-principals
}
return new ManagedIdentityCredential(); // or just DefaultAzureCredential
}
}
// Example: Using this for managed identity
var tokenCredential = tokencredentialFactory.GetTokenCredential();
var eventHubProducerClient =
new EventHubProducerClient(this.EventHubEndpoint, this.EventHubName, tokenCredential)
// Use this helper class when needing to access another resource protected by AzureAD
public class AzureAdTokenRetriever : IAzureAdTokenRetriever
{
private readonly ITokenCredentialFactory tokenCredentialFactory;
private readonly ILogger<AzureAdTokenRetriever> logger;
private readonly IMemoryCache inMemoryCache;
public AzureAdTokenRetriever(
ITokenCredentialFactory tokenCredentialFactory,
ILogger<AzureAdTokenRetriever> logger,
IMemoryCache inMemoryCache)
{
this.tokenCredentialFactory = tokenCredentialFactory;
this.logger = logger;
this.inMemoryCache = inMemoryCache;
}
public async Task<string> GetTokenAsync(string resourceId, string scope = "/.default")
{
var resourceIdentifier = resourceId + scope;
if (inMemoryCache.TryGetValue(resourceIdentifier, out var token))
{
this.logger.LogDebug("Token for {ResourceId} and {Scope} were fetched from cache", resourceId, scope);
return (string)token;
}
var tokenCredential = tokenCredentialFactory.GetTokenCredential();
this.logger.LogDebug("Token credential generated from MSI for {ResourceId} and {Scope}", resourceId, scope);
var accessToken = await tokenCredential.GetTokenAsync(
new TokenRequestContext(new [] { resourceIdentifier }), CancellationToken.None)
.ConfigureAwait(false);
// Set cache options with expiration 5 minutes before the token expires
var cacheEntryOptions = new MemoryCacheEntryOptions().SetAbsoluteExpiration(accessToken.ExpiresOn.AddMinutes(-5));
inMemoryCache.Set(resourceIdentifier, accessToken.Token, cacheEntryOptions);
this.logger.LogDebug("Token for {ResourceId} and {Scope} saved in cache with expiration of {TokenExpiry}",
resourceId, scope, cacheEntryOptions.AbsoluteExpiration);
return accessToken.Token;
}
}
// Example
var token = await azureAdTokenRetriever.GetTokenAsync("resource app id or uri", "required scopes");
var autheHeader = new AuthenticationHeaderValue("Bearer", token);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment