| File: Api\ApiAuthenticationHandler.cs | Web Access |
| Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Security.Claims; using System.Text.Encodings.Web; using Aspire.Dashboard.Authentication; using Aspire.Dashboard.Configuration; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; namespace Aspire.Dashboard.Api; /// <summary> /// Authentication handler for the Dashboard API that supports API key auth /// and falls back to frontend auth (browser token, OIDC, or unsecured based on configuration). /// </summary> /// <remarks> /// When Api.AuthMode is ApiKey, the API key is required for programmatic access. /// Browser-based access can still use frontend auth (browser token, OIDC). /// When Api.AuthMode is Unsecured, no authentication is required. /// </remarks> public sealed class ApiAuthenticationHandler( IOptionsMonitor<DashboardOptions> dashboardOptions, IOptionsMonitor<ApiAuthenticationHandlerOptions> options, ILoggerFactory loggerFactory, UrlEncoder encoder) : AuthenticationHandler<ApiAuthenticationHandlerOptions>(options, loggerFactory, encoder) { /// <summary> /// The header name for the Dashboard API key. /// </summary> public const string ApiKeyHeaderName = "x-api-key"; protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { var currentOptions = dashboardOptions.CurrentValue; var apiAuthMode = currentOptions.Api.AuthMode; // If API auth is unsecured, allow access if (apiAuthMode is ApiAuthMode.Unsecured) { var id = new ClaimsIdentity([new Claim(ClaimName, bool.TrueString)], AuthenticationScheme); return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(id), Scheme.Name)); } // If API auth requires API key if (apiAuthMode is ApiAuthMode.ApiKey) { var apiKeyBytes = currentOptions.Api.GetPrimaryApiKeyBytesOrNull(); // If ApiKey mode is set but no key is configured, fail authentication // rather than silently falling through to frontend auth if (apiKeyBytes is null) { return AuthenticateResult.Fail("API key authentication is enabled but no API key is configured."); } if (Context.Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyHeader)) { // There must be exactly one header with the API key. if (apiKeyHeader.Count != 1) { return AuthenticateResult.Fail("Invalid API key header."); } var providedApiKey = apiKeyHeader.ToString(); if (string.IsNullOrEmpty(providedApiKey)) { return AuthenticateResult.Fail("Invalid API key header."); } // Check primary key if (CompareHelpers.CompareKey(apiKeyBytes, providedApiKey)) { var id = new ClaimsIdentity([new Claim(ClaimName, bool.TrueString)], AuthenticationScheme); return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(id), Scheme.Name)); } // Check secondary key (for key rotation) if (currentOptions.Api.GetSecondaryApiKeyBytes() is { } secondaryBytes && CompareHelpers.CompareKey(secondaryBytes, providedApiKey)) { var id = new ClaimsIdentity([new Claim(ClaimName, bool.TrueString)], AuthenticationScheme); return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(id), Scheme.Name)); } return AuthenticateResult.Fail("Authentication failed."); } // API key header not provided - fall through to frontend auth for browser access } // Try frontend authentication (for browser-based access) var frontendResult = await Context.AuthenticateAsync(FrontendCompositeAuthenticationDefaults.AuthenticationScheme).ConfigureAwait(false); if (frontendResult.Succeeded) { return frontendResult; } // Return frontend failure if present if (frontendResult.Failure is not null) { return frontendResult; } return AuthenticateResult.NoResult(); } protected override Task HandleChallengeAsync(AuthenticationProperties properties) { // If an API key was provided (but was wrong), return 401 instead of redirecting if (Context.Request.Headers.ContainsKey(ApiKeyHeaderName)) { Context.Response.StatusCode = StatusCodes.Status401Unauthorized; return Task.CompletedTask; } // For browser-based access without API key, redirect to login var frontendAuthMode = dashboardOptions.CurrentValue.Frontend.AuthMode; var scheme = frontendAuthMode switch { FrontendAuthMode.OpenIdConnect => FrontendAuthenticationDefaults.AuthenticationSchemeOpenIdConnect, FrontendAuthMode.BrowserToken => FrontendAuthenticationDefaults.AuthenticationSchemeBrowserToken, _ => null }; if (scheme != null) { return Context.ChallengeAsync(scheme); } Context.Response.StatusCode = StatusCodes.Status401Unauthorized; return Task.CompletedTask; } public const string AuthenticationScheme = "Api"; public const string PolicyName = "ApiPolicy"; public const string ClaimName = "api"; } public sealed class ApiAuthenticationHandlerOptions : AuthenticationSchemeOptions { }