-
Notifications
You must be signed in to change notification settings - Fork 1.7k
.NET: DevUI: add configurable access controls for the DevUI HTTP surface #5739
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
117da53
.NET: DevUI: add configurable access controls for the DevUI HTTP surface
moonbox3 be72c90
.NET: DevUI: address review and fix dotnet format
moonbox3 20d23e4
.NET: DevUI: add missing authRequired param XML tag
moonbox3 aff38ee
.NET: DevUI tests: set loopback/AllowRemoteAccess for null-RemoteIp d…
moonbox3 b2c4809
.NET: DevUI tests: capture DEVUI_AUTH_TOKEN before parallel tests can…
moonbox3 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
106 changes: 106 additions & 0 deletions
106
dotnet/src/Microsoft.Agents.AI.DevUI/DevUIAuthFilter.cs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System.Net; | ||
| using System.Security.Cryptography; | ||
| using System.Text; | ||
| using Microsoft.Extensions.Options; | ||
| using Microsoft.Net.Http.Headers; | ||
|
|
||
| namespace Microsoft.Agents.AI.DevUI; | ||
|
|
||
| /// <summary> | ||
| /// Endpoint filter that enforces the DevUI security posture: loopback-only | ||
| /// access by default, plus optional bearer-token authentication. | ||
| /// </summary> | ||
| internal sealed class DevUIAuthFilter : IEndpointFilter | ||
| { | ||
| private const string BearerScheme = "Bearer"; | ||
|
|
||
| private readonly DevUIOptions _options; | ||
| private readonly byte[]? _expectedTokenBytes; | ||
| private readonly ILogger<DevUIAuthFilter> _logger; | ||
|
|
||
| /// <summary> | ||
| /// Gets a value indicating whether a bearer token is required by this filter | ||
| /// (either via <see cref="DevUIOptions.AuthToken"/> or the | ||
| /// <c>DEVUI_AUTH_TOKEN</c> environment variable). | ||
| /// </summary> | ||
| public bool TokenRequired => this._expectedTokenBytes is { Length: > 0 }; | ||
|
|
||
| public DevUIAuthFilter(IOptions<DevUIOptions> options, ILogger<DevUIAuthFilter> logger) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(options); | ||
| ArgumentNullException.ThrowIfNull(logger); | ||
| this._options = options.Value; | ||
| this._logger = logger; | ||
|
|
||
| var configuredToken = !string.IsNullOrEmpty(this._options.AuthToken) | ||
| ? this._options.AuthToken | ||
| : Environment.GetEnvironmentVariable(DevUIOptions.AuthTokenEnvironmentVariable); | ||
|
|
||
| this._expectedTokenBytes = !string.IsNullOrEmpty(configuredToken) | ||
| ? Encoding.UTF8.GetBytes(configuredToken) | ||
| : null; | ||
|
moonbox3 marked this conversation as resolved.
|
||
| } | ||
|
|
||
| public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) | ||
| { | ||
| var httpContext = context.HttpContext; | ||
| var remoteIp = httpContext.Connection.RemoteIpAddress; | ||
| var isLoopback = remoteIp is not null && IPAddress.IsLoopback(remoteIp); | ||
|
|
||
| if (!isLoopback && !this._options.AllowRemoteAccess) | ||
| { | ||
| this._logger.LogWarning( | ||
| "Rejected non-loopback DevUI request from {RemoteIp}. Set DevUIOptions.AllowRemoteAccess to permit remote callers.", | ||
| remoteIp); | ||
| return Results.Problem( | ||
| statusCode: StatusCodes.Status403Forbidden, | ||
| title: "DevUI access denied", | ||
| detail: "DevUI is restricted to loopback callers by default. Enable AllowRemoteAccess to permit remote access."); | ||
| } | ||
|
|
||
| if (this._expectedTokenBytes is { Length: > 0 } expected && !TokenIsValid(httpContext.Request, expected)) | ||
| { | ||
| httpContext.Response.Headers[HeaderNames.WWWAuthenticate] = BearerScheme; | ||
| return Results.Problem( | ||
| statusCode: StatusCodes.Status401Unauthorized, | ||
| title: "DevUI authentication required", | ||
| detail: "Provide a valid bearer token via the Authorization header."); | ||
| } | ||
|
|
||
| return await next(context).ConfigureAwait(false); | ||
| } | ||
|
|
||
| private static bool TokenIsValid(HttpRequest request, byte[] expected) | ||
| { | ||
| if (!request.Headers.TryGetValue(HeaderNames.Authorization, out var headerValues)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| foreach (var header in headerValues) | ||
| { | ||
| if (string.IsNullOrEmpty(header)) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| const int PrefixLength = 7; // "Bearer " | ||
| if (header.Length <= PrefixLength || | ||
| !header.StartsWith(BearerScheme, StringComparison.OrdinalIgnoreCase) || | ||
| header[BearerScheme.Length] != ' ') | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var presented = Encoding.UTF8.GetBytes(header.AsSpan(PrefixLength).Trim().ToString()); | ||
| if (CryptographicOperations.FixedTimeEquals(presented, expected)) | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
| } | ||
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| namespace Microsoft.Agents.AI.DevUI; | ||
|
|
||
| /// <summary> | ||
| /// Options that control the security posture of the DevUI HTTP surface. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// DevUI exposes agent metadata that is sensitive in production contexts: | ||
| /// system instructions, tool definitions, model identifiers, and workflow | ||
| /// structure. By default, DevUI rejects any request whose remote endpoint | ||
| /// is not a loopback address. Hosts that intentionally expose DevUI on a | ||
| /// non-loopback interface must opt in via <see cref="AllowRemoteAccess"/> | ||
| /// and should also configure <see cref="AuthToken"/> or | ||
| /// <see cref="ConfigureEndpoints"/> to attach an authorization policy. | ||
| /// </remarks> | ||
| public sealed class DevUIOptions | ||
| { | ||
| /// <summary> | ||
| /// Environment variable inspected for a default bearer token when | ||
| /// <see cref="AuthToken"/> is not explicitly set. | ||
| /// </summary> | ||
| public const string AuthTokenEnvironmentVariable = "DEVUI_AUTH_TOKEN"; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether DevUI may be served to | ||
| /// non-loopback callers. Defaults to <see langword="false"/>. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// When <see langword="false"/>, any request whose | ||
| /// <see cref="ConnectionInfo.RemoteIpAddress"/> is | ||
| /// not a loopback address (or is missing) is rejected with HTTP 403 before | ||
| /// reaching the DevUI handlers. Enable only when the host is responsible | ||
| /// for fronting DevUI with its own authentication, network policy, or both. | ||
| /// </remarks> | ||
| public bool AllowRemoteAccess { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a shared bearer token required on every DevUI request. | ||
| /// When <see langword="null"/> or empty, the value of the | ||
| /// <c>DEVUI_AUTH_TOKEN</c> environment variable is used instead. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// When a token is configured, requests must include the header | ||
| /// <c>Authorization: Bearer <token></c>. Comparison is performed | ||
| /// in constant time. This is a convenience for development scenarios. | ||
| /// Production hosts should prefer a real ASP.NET Core authentication | ||
| /// scheme attached via <see cref="ConfigureEndpoints"/>. | ||
| /// </remarks> | ||
| public string? AuthToken { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a callback invoked with the DevUI endpoint group so the | ||
| /// host can attach authorization, rate limiting, or other endpoint | ||
| /// conventions (for example | ||
| /// <c>group.RequireAuthorization("DevUIPolicy")</c>). | ||
| /// </summary> | ||
| public Action<IEndpointConventionBuilder>? ConfigureEndpoints { get; set; } | ||
| } |
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
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
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
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
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.