Created
October 15, 2025 12:52
-
-
Save cobysy/c12c1a6b656b20c3007b137b6c1049b0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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