Skip to content

Instantly share code, notes, and snippets.

@cobysy
Created October 15, 2025 12:52
Show Gist options
  • Save cobysy/c12c1a6b656b20c3007b137b6c1049b0 to your computer and use it in GitHub Desktop.
Save cobysy/c12c1a6b656b20c3007b137b6c1049b0 to your computer and use it in GitHub Desktop.
public sealed class AzureCliCredentialProxy(IHttpClientFactory httpClientFactory)
: DefaultAzureCredential
{
public override AccessToken GetToken(
TokenRequestContext requestContext,
CancellationToken cancellationToken = default)
{
return this.GetTokenAsync(
requestContext,
cancellationToken).GetAwaiter().GetResult();
}
public override async ValueTask<AccessToken> GetTokenAsync(
TokenRequestContext requestContext,
CancellationToken cancellationToken = default)
{
// AZURECLICREDENTIALPROXY is either configured in Debug Launch Profile in Visual Studio or in docker-compose (never use this in Azure)
var azureCliCredentialProxy = Environment.GetEnvironmentVariable("AZURECLICREDENTIALPROXY");
if (!string.IsNullOrWhiteSpace(azureCliCredentialProxy))
{
return await this.GetAccessTokenUsingAzureCliCredentialProxyAsync(
requestContext,
cancellationToken,
azureCliCredentialProxy);
}
return await base.GetTokenAsync(
requestContext,
cancellationToken);
}
/// <summary>
/// Equivalent to C# AzureCliCredential class
/// except that this uses a node.js proxy on the (Docker) host to acquire a token.
/// </summary>
private async ValueTask<AccessToken> GetAccessTokenUsingAzureCliCredentialProxyAsync(
TokenRequestContext requestContext,
CancellationToken cancellationToken,
string azureCliProxy)
{
using var httpClient = httpClientFactory.CreateClient();
var resource = requestContext.Scopes[0];
var requestUri = $"{azureCliProxy}?resource={Uri.EscapeDataString(resource)}";
using var request = new HttpRequestMessage(
HttpMethod.Get,
requestUri);
using var response = await httpClient.SendAsync(
request,
cancellationToken);
response.EnsureSuccessStatusCode();
var jsonContent = await response.Content.ReadAsStringAsync(cancellationToken);
// Parse JSON response similar to AzureCliCredential
return DeserializeOutput(jsonContent);
}
/// <summary>
/// Copied and pasted from AzureCliCredential class
/// </summary>
/// <param name="output"></param>
/// <returns></returns>
private static AccessToken DeserializeOutput(
string output)
{
using var document = JsonDocument.Parse(output);
var root = document.RootElement;
var accessToken = root.GetProperty("accessToken").GetString();
var expiresOn = root.TryGetProperty(
"expires_on",
out var expires_on)
? DateTimeOffset.FromUnixTimeSeconds(expires_on.GetInt64())
: DateTimeOffset.ParseExact(
root.GetProperty("expiresOn").GetString(),
"yyyy-MM-dd HH:mm:ss.ffffff",
CultureInfo.InvariantCulture,
DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeLocal);
return new AccessToken(
accessToken,
expiresOn);
}
}
import express from "express";
import { exec } from "child_process";
import { promisify } from "util";
const app = express();
const port = 3000;
const execAsync = promisify(exec);
// Scope validation constants
const DEFAULT_SUFFIX = "/.default";
const SCOPE_PATTERN = /^[0-9a-zA-Z-_.:/]+$/;
const INVALID_SCOPE_MESSAGE = "The specified scope is not in expected format. Only alphanumeric characters, '.', '-', ':', '_', and '/' are allowed";
// Helper function to get current timestamp
function getTimestamp() {
return new Date().toISOString();
}
// Convert scopes to resource (equivalent to C# ScopesToResource method)
function scopesToResource(scopes) {
if (!scopes) {
throw new Error("scopes parameter is required");
}
if (!Array.isArray(scopes) || scopes.length !== 1) {
throw new Error("To convert to a resource string the specified array must be exactly length 1");
}
const scope = scopes[0];
// Validate scope format
if (!SCOPE_PATTERN.test(scope)) {
throw new Error(INVALID_SCOPE_MESSAGE);
}
if (!scope.endsWith(DEFAULT_SUFFIX)) {
return scope;
}
return scope.substring(0, scope.lastIndexOf(DEFAULT_SUFFIX));
}
// Middleware to parse JSON bodies
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Middleware to log all incoming requests
app.use((req, res, next) => {
console.log(`[${getTimestamp()}] ${req.method} ${req.url} - Request received`);
console.log(`[${getTimestamp()}] ----------------------------------------`);
next();
});
app.get("/", async (req, res) => {
try {
// Get scope from query parameter or use default
const scope = req.query.resource || "https://management.azure.com/.default";
console.log(`[${getTimestamp()}] Processing token request for scope: ${scope}`);
// Convert scope to resource using validation logic
const resource = scopesToResource([scope]);
console.log(`[${getTimestamp()}] Converted scope to resource: ${scope} -> ${resource}`);
console.log(`[${getTimestamp()}] Getting token via Azure CLI for resource: ${resource}`);
// Equivalent to C# AzureCliCredential class
const azCommand = `az account get-access-token --resource ${resource} --output json`;
console.log(`[${getTimestamp()}] Executing: ${azCommand}`);
const { stdout } = await execAsync(azCommand);
console.log(`[${getTimestamp()}] Token obtained successfully`);
console.log(`[${getTimestamp()}] Azure CLI output:`, stdout);
res.send(stdout);
console.log(`[${getTimestamp()}] Token response sent to client`);
console.log(`[${getTimestamp()}] ----------------------------------------`);
} catch (err) {
console.error(`[${getTimestamp()}] Error fetching token:`, err);
res.status(500).json({ error: err.message });
}
});
app.listen(port, () => {
console.log(`AzureCliCredentialProxy running on http://localhost:${port}`);
});
export AZURECLICREDENTIALPROXY=http://host.docker.internal:3000
docker-compose up --build
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment