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 };