Skip to content
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

[release/8.0] Add token auth to dashboard frontend #3350

Merged
merged 4 commits into from
Apr 4, 2024
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
15 changes: 15 additions & 0 deletions src/Aspire.Dashboard/Aspire.Dashboard.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Resources\Login.Designer.cs">
<DependentUpon>Login.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<Compile Update="Resources\StructuredLogs.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
Expand Down Expand Up @@ -142,6 +147,13 @@
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Login.resx">
<XlfOutputItem>EmbeddedResource</XlfOutputItem>
<SubType>Designer</SubType>
<LastGenOutput>Login.Designer.cs</LastGenOutput>
<XlfSourceFormat>Resx</XlfSourceFormat>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resources\StructuredLogs.resx">
<XlfSourceFormat>Resx</XlfSourceFormat>
<XlfOutputItem>EmbeddedResource</XlfOutputItem>
Expand Down Expand Up @@ -186,5 +198,8 @@
<Compile Include="$(SharedDir)Model\KnownResourceTypes.cs" Link="Utils\KnownResourceTypes.cs" />
<Compile Include="$(SharedDir)CircularBuffer.cs" Link="Otlp\Storage\CircularBuffer.cs" />
<Compile Include="$(SharedDir)DashboardConfigNames.cs" Link="Utils\DashboardConfigNames.cs" />
<Compile Include="$(SharedDir)TokenGenerator.cs" Link="Utils\TokenGenerator.cs" />
<Compile Include="$(SharedDir)LoggingHelpers.cs" Link="Utils\LoggingHelpers.cs" />
<Compile Include="$(SharedDir)StringUtils.cs" Link="Utils\StringUtils.cs" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;

Expand Down Expand Up @@ -34,9 +32,9 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
return Task.FromResult(AuthenticateResult.Fail($"Multiple '{ApiKeyHeaderName}' headers in request."));
}

if (!CompareApiKey(options.GetPrimaryApiKeyBytes(), apiKey.ToString()))
if (!CompareHelpers.CompareKey(options.GetPrimaryApiKeyBytes(), apiKey.ToString()))
{
if (options.GetSecondaryApiKeyBytes() is not { } secondaryBytes || !CompareApiKey(secondaryBytes, apiKey.ToString()))
if (options.GetSecondaryApiKeyBytes() is not { } secondaryBytes || !CompareHelpers.CompareKey(secondaryBytes, apiKey.ToString()))
{
return Task.FromResult(AuthenticateResult.Fail($"Incoming API key from '{ApiKeyHeaderName}' header doesn't match configured API key."));
}
Expand All @@ -49,50 +47,6 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()

return Task.FromResult(AuthenticateResult.NoResult());
}

// This method is used to compare two API keys in a way that avoids timing attacks.
private static bool CompareApiKey(byte[] expectedApiKeyBytes, string requestApiKey)
{
const int StackAllocThreshold = 256;

var requestByteCount = Encoding.UTF8.GetByteCount(requestApiKey);

// API key will never match if lengths are different. But still do all the work to avoid timing attacks.
var lengthsEqual = expectedApiKeyBytes.Length == requestByteCount;

var requestSpanLength = Math.Max(requestByteCount, expectedApiKeyBytes.Length);
byte[]? requestPooled = null;
var requestBytesSpan = (requestSpanLength <= StackAllocThreshold ?
stackalloc byte[StackAllocThreshold] :
(requestPooled = RentClearedArray(requestSpanLength))).Slice(0, requestSpanLength);

try
{
// Always succeeds because the byte span is always as big or bigger than required.
Encoding.UTF8.GetBytes(requestApiKey, requestBytesSpan);

// Trim request bytes to the same length as expected bytes. Need to be the same size for fixed time comparison.
var equals = CryptographicOperations.FixedTimeEquals(expectedApiKeyBytes, requestBytesSpan.Slice(0, expectedApiKeyBytes.Length));

return equals && lengthsEqual;
}
finally
{
if (requestPooled != null)
{
ArrayPool<byte>.Shared.Return(requestPooled);
}
}

static byte[] RentClearedArray(int byteCount)
{
// UTF8 bytes are copied into the array but remaining bytes are untouched.
// Because all bytes in the array are compared, clear the array to avoid comparing previous data.
var array = ArrayPool<byte>.Shared.Rent(byteCount);
Array.Clear(array);
return array;
}
}
}

public static class OtlpApiKeyAuthenticationDefaults
Expand Down
95 changes: 95 additions & 0 deletions src/Aspire.Dashboard/Components/Controls/AspireLogo.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<svg width="@Height" height="@Width" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_449_831" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="22">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.39001 12C4.49001 12 3.67 12.4799 3.22 13.2499L6.67 7.27994L6.6817 7.25982L9.84001 1.79005C10.05 1.43005 10.36 1.11005 10.75 0.880049C11.14 0.650049 11.57 0.550049 12 0.550049C12.86 0.550049 13.7 0.990049 14.17 1.80005L17.33 7.28005L23.67 18.25C23.88 18.62 24 19.05 24 19.5C24 20.88 22.88 22 21.5 22H8.27002C8.27001 22 8.27002 22 8.27002 22H2.5C1.12 22 0 20.88 0 19.5C0 19.05 0.12 18.62 0.33 18.25L3.22 13.2499C3.67 12.4799 4.49001 12 5.39001 12C5.39002 12 5.39001 12 5.39001 12Z" fill="url(#paint0_linear_449_831)" />
</mask>
<g mask="url(#mask0_449_831)">
<path d="M20.06 12H13.72L11 7.28005C10.79 6.91005 10.48 6.59005 10.08 6.37005C8.88998 5.67005 7.35998 6.08005 6.66998 7.28005L9.83998 1.79005C10.05 1.43005 10.36 1.11005 10.75 0.880049C11.14 0.650049 11.57 0.550049 12 0.550049C12.86 0.550049 13.7 0.990049 14.17 1.80005L17.33 7.28005L20.06 12Z" fill="url(#paint1_linear_449_831)" />
<g filter="url(#filter0_dd_449_831)">
<path d="M5.38997 11.9999H13.72L11 7.27994C10.79 6.90994 10.48 6.58994 10.08 6.36994C8.88997 5.66994 7.35997 6.07994 6.66997 7.27994L3.21997 13.2499C3.66997 12.4799 4.48997 11.9999 5.38997 11.9999Z" fill="url(#paint2_linear_449_831)" />
<path d="M21.5 22C22.88 22 24 20.88 24 19.5C24 19.05 23.88 18.62 23.67 18.25L20.06 12L13.72 11.9999L17.33 18.25C17.55 18.62 17.67 19.05 17.67 19.5C17.67 20.88 16.55 22 15.17 22H21.5Z" fill="url(#paint3_linear_449_831)" />
</g>
<g filter="url(#filter1_dd_449_831)">
<path d="M17.67 19.5C17.67 20.88 16.55 22 15.17 22H8.27002C9.65002 22 10.77 20.88 10.77 19.5C10.77 19.05 10.65 18.62 10.44 18.25L7.55001 13.25C7.52002 13.19 7.48001 13.14 7.44001 13.08C6.99001 12.42 6.23001 12 5.39001 12H13.72L17.33 18.25C17.55 18.62 17.67 19.05 17.67 19.5Z" fill="url(#paint4_linear_449_831)" />
</g>
<g filter="url(#filter2_dd_449_831)">
<path d="M10.77 19.5C10.77 20.88 9.65 22 8.27 22H2.5C1.12 22 0 20.88 0 19.5C0 19.05 0.12 18.62 0.33 18.25L3.22 13.25C3.67 12.48 4.49 12 5.39 12C6.23 12 6.99 12.42 7.44 13.08C7.48 13.14 7.52 13.19 7.55 13.25L10.44 18.25C10.65 18.62 10.77 19.05 10.77 19.5Z" fill="url(#paint5_linear_449_831)" />
</g>
</g>
<defs>
<filter id="filter0_dd_449_831" x="1.21997" y="4.52808" width="24.78" height="19.9719" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dy="0.095" />
<feGaussianBlur stdDeviation="0.095" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_449_831" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dy="0.5" />
<feGaussianBlur stdDeviation="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.32 0" />
<feBlend mode="normal" in2="effect1_dropShadow_449_831" result="effect2_dropShadow_449_831" />
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_449_831" result="shape" />
</filter>
<filter id="filter1_dd_449_831" x="3.39001" y="10.5" width="16.28" height="14" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dy="0.095" />
<feGaussianBlur stdDeviation="0.095" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_449_831" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dy="0.5" />
<feGaussianBlur stdDeviation="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.32 0" />
<feBlend mode="normal" in2="effect1_dropShadow_449_831" result="effect2_dropShadow_449_831" />
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_449_831" result="shape" />
</filter>
<filter id="filter2_dd_449_831" x="-2" y="10.5" width="14.77" height="14" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dy="0.095" />
<feGaussianBlur stdDeviation="0.095" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_449_831" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dy="0.5" />
<feGaussianBlur stdDeviation="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.32 0" />
<feBlend mode="normal" in2="effect1_dropShadow_449_831" result="effect2_dropShadow_449_831" />
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_449_831" result="shape" />
</filter>
<linearGradient id="paint0_linear_449_831" x1="1.88475" y1="11.1667" x2="10.31" y2="23.1443" gradientUnits="userSpaceOnUse">
<stop stop-color="#CBBFF2" />
<stop offset="1" stop-color="#B9AAEE" />
</linearGradient>
<linearGradient id="paint1_linear_449_831" x1="9.6127" y1="-0.685575" x2="16.8764" y2="13.8912" gradientUnits="userSpaceOnUse">
<stop stop-color="#7455DD" />
<stop stop-color="#6745DA" />
<stop offset="1" stop-color="#512BD4" />
</linearGradient>
<linearGradient id="paint2_linear_449_831" x1="7.90532" y1="3.78438" x2="19.1767" y2="23.0023" gradientUnits="userSpaceOnUse">
<stop stop-color="#856AE1" />
<stop offset="1" stop-color="#7455DD" />
</linearGradient>
<linearGradient id="paint3_linear_449_831" x1="7.90532" y1="3.78438" x2="19.1767" y2="23.0023" gradientUnits="userSpaceOnUse">
<stop stop-color="#856AE1" />
<stop offset="1" stop-color="#7455DD" />
</linearGradient>
<linearGradient id="paint4_linear_449_831" x1="5.4257" y1="9.22222" x2="13.2216" y2="21.4193" gradientUnits="userSpaceOnUse">
<stop stop-color="#A895E9" />
<stop offset="1" stop-color="#9780E5" />
</linearGradient>
<linearGradient id="paint5_linear_449_831" x1="1.88475" y1="11.1667" x2="10.31" y2="23.1443" gradientUnits="userSpaceOnUse">
<stop stop-color="#CBBFF2" />
<stop offset="1" stop-color="#B9AAEE" />
</linearGradient>
</defs>
</svg>

@code {
[Parameter]
public int Height { get; set; } = 24;

[Parameter]
public int Width { get; set; } = 24;
}
3 changes: 2 additions & 1 deletion src/Aspire.Dashboard/Components/Dialogs/SettingsDialog.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@using Aspire.Dashboard.Resources
@using Aspire.Dashboard.Utils
@inject IStringLocalizer<Dialogs> Loc

<FluentStack Orientation="Orientation.Vertical" Style="display: contents">
Expand All @@ -8,5 +9,5 @@
<FluentRadio Value="@Aspire.Dashboard.Model.ThemeManager.ThemeSettingDark">@Loc[nameof(Dialogs.SettingsDialogDarkTheme)]</FluentRadio>
</FluentRadioGroup>
<div style="height: 100%;"></div>
<div class="version">@string.Format(Loc[nameof(Dialogs.SettingsDialogVersion)], s_version)</div>
<div class="version">@string.Format(Loc[nameof(Dialogs.SettingsDialogVersion)], VersionHelpers.DashboardDisplayVersion)</div>
</FluentStack>
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Dashboard.Extensions;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Components;
Expand All @@ -13,7 +12,6 @@ namespace Aspire.Dashboard.Components.Dialogs;
public partial class SettingsDialog : IDialogContentComponent, IAsyncDisposable
{
private string _currentSetting = ThemeManager.ThemeSettingSystem;
private static readonly string? s_version = typeof(SettingsDialog).Assembly.GetDisplayVersion();

private IJSObjectReference? _jsModule;
private IDisposable? _themeChangedSubscription;
Expand Down
3 changes: 3 additions & 0 deletions src/Aspire.Dashboard/Components/Layout/EmptyLayout.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@inherits LayoutComponentBase
@Body
<FluentTooltipProvider />
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
IconRest="ResourcesIcon()"
IconActive="ResourcesIcon(active: true)"
Text="@Loc[nameof(Layout.NavMenuResourcesTab)]" />
<FluentAppBarItem Href="@DashboardUrls.ConsoleLogsUrl()"
<FluentAppBarItem Href="@DashboardUrls.ConsoleLogsUrl()"
IconRest="ConsoleLogsIcon()"
IconActive="ConsoleLogsIcon(active: true)"
Text="@Loc[nameof(Layout.NavMenuConsoleLogsTab)]" />
Expand Down
49 changes: 49 additions & 0 deletions src/Aspire.Dashboard/Components/Pages/Login.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@page "/login"
@using Aspire.Dashboard.Utils
@layout EmptyLayout
@attribute [AllowAnonymous]
@inject IStringLocalizer<Dashboard.Resources.Login> Loc

<PageTitle><ApplicationName ResourceName="@nameof(Dashboard.Resources.Login.PageTitle)" Loc="@Loc" /></PageTitle>

<div class="token-backdrop">
<EditForm EditContext="@EditContext" OnValidSubmit="@SubmitAsync" FormName="token_validation">
<DataAnnotationsValidator />
<div class="token-form-container">
<div class="token-logo">
<AspireLogo Height="128" Width="128" />
</div>
<div class="token-entry-container">
<div class="token-entry-header">
<h1><ApplicationName ResourceName="@nameof(Dashboard.Resources.Login.Header)" Loc="@Loc" /></h1>
</div>
<div class="token-entry">
<FluentTextField @ref="_tokenTextField" Id="token-text-field" @bind-Value="_formModel.Token"
Placeholder="@Loc[nameof(Dashboard.Resources.Login.TextFieldPlaceholder)]"
TextFieldType="TextFieldType.Password" Class="token-entry-text" />
</div>
<div class="token-validation">
<FluentValidationMessage For="() => _formModel.Token" />
</div>
<div class="token-entry-footer">
<a href="javascript:;" id="helpLink">@Loc[nameof(Dashboard.Resources.Login.WhereIsMyTokenLinkText)]</a>
<FluentButton Appearance="Appearance.Accent" Type="ButtonType.Submit">@Loc[nameof(Dashboard.Resources.Login.LogInButtonText)]</FluentButton>
<FluentTooltip Anchor="helpLink" Position="TooltipPosition.Bottom">
<div class="token-help-container">
<div class="token-help-text">@Loc[nameof(Dashboard.Resources.Login.HelpPopupText)]</div>
<img class="token-help-image" src="/img/TokenExample.png"
alt="@Loc[nameof(Dashboard.Resources.Login.HelpScreenshotAltText)]" />
<FluentAnchor Href="https://go.microsoft.com/fwlink/?linkid=2265718" Target="_blank"
Appearance="Appearance.Hypertext">
@Loc[nameof(Dashboard.Resources.Login.MoreInfoLinkText)]
</FluentAnchor>
</div>
</FluentTooltip>
</div>
</div>
</div>
</EditForm>
<div class="version-info">
@VersionHelpers.DashboardDisplayVersion
</div>
</div>
Loading