Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/AzLocal.Client/AzlocalClientFactory.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using AzLocal.Core;
using Azure.Security.KeyVault.Secrets;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
Expand All @@ -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;

/// <summary>The credential that satisfies Azure SDK authentication against the local emulator.</summary>
public AzlocalCredential Credential { get; } = new();
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down
7 changes: 5 additions & 2 deletions src/AzLocal.Client/AzlocalCredential.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using AzLocal.Core;
using Azure.Core;

namespace AzLocal.Client;
Expand All @@ -9,8 +10,10 @@ namespace AzLocal.Client;
/// </summary>
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;
Expand Down
14 changes: 14 additions & 0 deletions src/AzLocal.Core/EmulatorDefaults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace AzLocal.Core;

/// <summary>
/// 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.
/// </summary>
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";
}
3 changes: 2 additions & 1 deletion src/AzLocal.Host/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
11 changes: 7 additions & 4 deletions src/AzLocal.Host/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": ""
}
}
28 changes: 18 additions & 10 deletions src/AzLocal.Middleware/AuthStubMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using AzLocal.Core;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using System.Security.Claims;

namespace AzLocal.Middleware;
Expand All @@ -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 <c>AzLocal:ObjectId</c> and <c>AzLocal:TenantId</c> in configuration.
/// </summary>
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);
}
}
37 changes: 23 additions & 14 deletions src/AzLocal.Middleware/ImdsMiddleware.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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 <c>AzLocal:FakeToken</c> in configuration.
/// </summary>
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);
}
}


2 changes: 1 addition & 1 deletion src/AzLocal.Services/Arm/SubscriptionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public SubscriptionHandler(IConfiguration config, ILogger<SubscriptionHandler> l
_fakeSubscription = new
{
subscriptionId = _subscriptionId,
displayName = "AzLocal Dev Subscription",
displayName = config["AzLocal:SubscriptionDisplayName"] ?? "AzLocal Dev Subscription",
state = "Enabled",
tenantId
};
Expand Down
Loading