diff --git a/src/AzLocal.Client/AzlocalClientFactory.cs b/src/AzLocal.Client/AzlocalClientFactory.cs
index ccf15dd..73eb20c 100644
--- a/src/AzLocal.Client/AzlocalClientFactory.cs
+++ b/src/AzLocal.Client/AzlocalClientFactory.cs
@@ -1,3 +1,4 @@
+using AzLocal.Core;
using Azure.Security.KeyVault.Secrets;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
@@ -12,7 +13,7 @@ namespace AzLocal.Client;
public sealed class AzlocalClientFactory
{
private readonly string _baseUrl;
- private const string DefaultBaseUrl = "http://localhost:4566";
+ private const string DefaultBaseUrl = EmulatorDefaults.BaseUrl;
/// The credential that satisfies Azure SDK authentication against the local emulator.
public AzlocalCredential Credential { get; } = new();
@@ -108,7 +109,7 @@ public HttpClient CreateServiceBusHttpClient(string @namespace)
BaseAddress = new Uri($"{_baseUrl}/sb/{@namespace}/")
};
client.DefaultRequestHeaders.Authorization =
- new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "fake-azlocal-token");
+ new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", AzlocalCredential.TokenValue);
return client;
}
@@ -124,7 +125,7 @@ public HttpClient CreateHttpClient()
{
var client = new HttpClient { BaseAddress = new Uri(_baseUrl) };
client.DefaultRequestHeaders.Authorization =
- new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "fake-azlocal-token");
+ new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", AzlocalCredential.TokenValue);
return client;
}
diff --git a/src/AzLocal.Client/AzlocalCredential.cs b/src/AzLocal.Client/AzlocalCredential.cs
index 7c13b57..262a2da 100644
--- a/src/AzLocal.Client/AzlocalCredential.cs
+++ b/src/AzLocal.Client/AzlocalCredential.cs
@@ -1,3 +1,4 @@
+using AzLocal.Core;
using Azure.Core;
namespace AzLocal.Client;
@@ -9,8 +10,10 @@ namespace AzLocal.Client;
///
public sealed class AzlocalCredential : TokenCredential
{
- // Matches the token returned by ImdsMiddleware so the SDK sees a consistent value.
- private static readonly AccessToken Token = new("fake-azlocal-token", DateTimeOffset.MaxValue);
+ // Token value is shared with ImdsMiddleware via EmulatorDefaults — they must always match.
+ public static readonly string TokenValue = EmulatorDefaults.FakeToken;
+
+ private static readonly AccessToken Token = new(TokenValue, DateTimeOffset.MaxValue);
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> Token;
diff --git a/src/AzLocal.Core/EmulatorDefaults.cs b/src/AzLocal.Core/EmulatorDefaults.cs
new file mode 100644
index 0000000..b1239ed
--- /dev/null
+++ b/src/AzLocal.Core/EmulatorDefaults.cs
@@ -0,0 +1,14 @@
+namespace AzLocal.Core;
+
+///
+/// Single source of truth for default values shared across the emulator host and client library.
+/// All defaults match the values in appsettings.json — change them there to override at runtime.
+///
+public static class EmulatorDefaults
+{
+ public const string FakeToken = "fake-azlocal-token";
+ public const string BaseUrl = "http://localhost:4566";
+ public const string ObjectId = "00000000-0000-0000-0000-000000000001";
+ public const string TenantId = "00000000-0000-0000-0000-000000000002";
+ public const string SubscriptionId = "00000000-0000-0000-0000-000000000001";
+}
diff --git a/src/AzLocal.Host/Program.cs b/src/AzLocal.Host/Program.cs
index 812dda6..6d8c43b 100644
--- a/src/AzLocal.Host/Program.cs
+++ b/src/AzLocal.Host/Program.cs
@@ -22,11 +22,12 @@
app.Logger.LogInformation("Registered service: {ServiceName}", handler.ServiceName);
}
+var baseUrl = app.Configuration["AzLocal:BaseUrl"] ?? "http://localhost:4566";
app.MapGet("/", () => new
{
status = "AzLocal emulator running",
version = "1.0",
- docs = "http://localhost:4566/"
+ docs = $"{baseUrl}/"
});
app.Run();
diff --git a/src/AzLocal.Host/appsettings.json b/src/AzLocal.Host/appsettings.json
index 161c961..f5eb914 100644
--- a/src/AzLocal.Host/appsettings.json
+++ b/src/AzLocal.Host/appsettings.json
@@ -10,11 +10,14 @@
"AzLocal": {
"StateMode": "InMemory",
"BaseUrl": "http://localhost:4566",
+ "FakeToken": "fake-azlocal-token",
+ "ObjectId": "00000000-0000-0000-0000-000000000001",
+ "TenantId": "00000000-0000-0000-0000-000000000002",
+ "SubscriptionId": "00000000-0000-0000-0000-000000000001",
+ "SubscriptionDisplayName": "AzLocal Dev Subscription",
+ "DefaultLocation": "eastus",
"BlobStorePath": "",
"SnapshotPath": "",
- "SqlitePath": "",
- "SubscriptionId": "00000000-0000-0000-0000-000000000001",
- "TenantId": "00000000-0000-0000-0000-000000000002",
- "DefaultLocation": "eastus"
+ "SqlitePath": ""
}
}
diff --git a/src/AzLocal.Middleware/AuthStubMiddleware.cs b/src/AzLocal.Middleware/AuthStubMiddleware.cs
index 61f9d55..d402aae 100644
--- a/src/AzLocal.Middleware/AuthStubMiddleware.cs
+++ b/src/AzLocal.Middleware/AuthStubMiddleware.cs
@@ -1,4 +1,6 @@
+using AzLocal.Core;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
using System.Security.Claims;
namespace AzLocal.Middleware;
@@ -7,26 +9,32 @@ namespace AzLocal.Middleware;
/// Bypasses Azure AD authentication for local development.
/// Injects a fake authenticated identity on every request so [Authorize] checks pass
/// without needing a real Azure AD token or tenant.
+/// Identity values are read from AzLocal:ObjectId and AzLocal:TenantId in configuration.
///
public class AuthStubMiddleware
{
private readonly RequestDelegate _next;
+ private readonly ClaimsPrincipal _fakeUser;
- // Static so the same fake identity is reused on every request — no per-request allocation.
- // authenticationType must be non-null/non-empty for IsAuthenticated to return true.
- private static readonly ClaimsPrincipal FakeUser = new(new ClaimsIdentity(
- [
- new Claim("oid", "00000000-0000-0000-0000-000000000001"), // fake Azure object ID
- new Claim("tid", "00000000-0000-0000-0000-000000000002"), // fake tenant ID
- new Claim(ClaimTypes.Name, "azlocal-dev"),
- ], authenticationType: "AzLocalStub"));
+ public AuthStubMiddleware(RequestDelegate next, IConfiguration config)
+ {
+ _next = next;
+ var objectId = config["AzLocal:ObjectId"] ?? EmulatorDefaults.ObjectId;
+ var tenantId = config["AzLocal:TenantId"] ?? EmulatorDefaults.TenantId;
- public AuthStubMiddleware(RequestDelegate next) => _next = next;
+ // authenticationType must be non-null/non-empty for IsAuthenticated to return true.
+ _fakeUser = new ClaimsPrincipal(new ClaimsIdentity(
+ [
+ new Claim("oid", objectId),
+ new Claim("tid", tenantId),
+ new Claim(ClaimTypes.Name, "azlocal-dev"),
+ ], authenticationType: "AzLocalStub"));
+ }
public async Task InvokeAsync(HttpContext context)
{
// Skip real token validation — stamp every request as authenticated locally.
- context.User = FakeUser;
+ context.User = _fakeUser;
await _next(context);
}
}
diff --git a/src/AzLocal.Middleware/ImdsMiddleware.cs b/src/AzLocal.Middleware/ImdsMiddleware.cs
index 60a5d0e..e37aba3 100644
--- a/src/AzLocal.Middleware/ImdsMiddleware.cs
+++ b/src/AzLocal.Middleware/ImdsMiddleware.cs
@@ -1,33 +1,42 @@
-using Microsoft.AspNetCore.Http;
+using AzLocal.Core;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
using System.Text.Json;
namespace AzLocal.Middleware;
+///
+/// Intercepts Azure IMDS (Instance Metadata Service) token requests so the Azure SDK
+/// can acquire a credential locally without a real Azure VM or managed identity.
+/// Token value is read from AzLocal:FakeToken in configuration.
+///
public class ImdsMiddleware
{
private readonly RequestDelegate _next;
+ private readonly string _fakeTokenJson;
- private static readonly string FakeTokenJson = JsonSerializer.Serialize(new
+ public ImdsMiddleware(RequestDelegate next, IConfiguration config)
{
- access_token = "fake-azlocal-token",
- expires_on = "99999999999",
- resource = "https://management.azure.com/",
- token_type = "Bearer"
- });
-
- public ImdsMiddleware(RequestDelegate next) => _next = next;
+ _next = next;
+ var token = config["AzLocal:FakeToken"] ?? EmulatorDefaults.FakeToken;
+ _fakeTokenJson = JsonSerializer.Serialize(new
+ {
+ access_token = token,
+ expires_on = "99999999999",
+ resource = "https://management.azure.com/",
+ token_type = "Bearer"
+ });
+ }
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Path.StartsWithSegments("/metadata/identity/oauth2/token"))
{
context.Response.ContentType = "application/json";
- context.Response.StatusCode = 200;
- await context.Response.WriteAsync(FakeTokenJson);
- return; // Do NOT call _next — response is already written
+ context.Response.StatusCode = 200;
+ await context.Response.WriteAsync(_fakeTokenJson);
+ return; // Do NOT call _next — response is already written.
}
await _next(context);
}
}
-
-
diff --git a/src/AzLocal.Services/Arm/SubscriptionHandler.cs b/src/AzLocal.Services/Arm/SubscriptionHandler.cs
index 46bd900..43b1ec6 100644
--- a/src/AzLocal.Services/Arm/SubscriptionHandler.cs
+++ b/src/AzLocal.Services/Arm/SubscriptionHandler.cs
@@ -28,7 +28,7 @@ public SubscriptionHandler(IConfiguration config, ILogger l
_fakeSubscription = new
{
subscriptionId = _subscriptionId,
- displayName = "AzLocal Dev Subscription",
+ displayName = config["AzLocal:SubscriptionDisplayName"] ?? "AzLocal Dev Subscription",
state = "Enabled",
tenantId
};