Merge pull request #1998 from DuendeSoftware/ka-fix-WWWAuthenticate

Ensure WWW-Authenticate uses a single HTTP header
This commit is contained in:
Joe DeCock 2025-05-06 20:51:18 -05:00 committed by GitHub
commit 97927d9219
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 90 additions and 9 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# Visual Studio Code workspace options
.vscode/settings.json

View file

@ -27,6 +27,7 @@
<!--tests -->
<PackageReference Update="FluentAssertions" Version="6.5.1"/>
<PackageReference Update="FluentAssertions.Web" Version="1.5.0"/>
<PackageReference Update="Shouldly" Version="4.2.1" />
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Update="xunit" Version="2.9.0"/>
<PackageReference Update="xunit.runner.visualstudio" Version="2.5.4" PrivateAssets="All"/>

View file

@ -9,6 +9,7 @@ using Microsoft.Extensions.Primitives;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Duende.IdentityModel;
using System.Collections.Generic;
namespace Duende.IdentityServer.Endpoints.Results;
@ -59,16 +60,24 @@ internal class ProtectedResourceErrorHttpWriter : IHttpResponseWriter<ProtectedR
errorDescription = "The access token expired";
}
var errorString = string.Format($"error=\"{error}\"");
if (errorDescription.IsMissing())
var values = new List<string>
{
context.Response.Headers.Append(HeaderNames.WWWAuthenticate, new StringValues(new[] { "Bearer realm=\"IdentityServer\"", errorString }));
}
else
"""
Bearer realm="IdentityServer"
""",
$"""
error="{error}"
"""
};
if (!errorDescription.IsMissing())
{
var errorDescriptionString = string.Format($"error_description=\"{errorDescription}\"");
context.Response.Headers.Append(HeaderNames.WWWAuthenticate, new StringValues(new[] { "Bearer realm=\"IdentityServer\"", errorString, errorDescriptionString }));
values.Add($"""
error_description="{errorDescription}"
""");
}
context.Response.Headers.Append(HeaderNames.WWWAuthenticate, string.Join(",", values));
return Task.CompletedTask;
}

View file

@ -23,6 +23,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Shouldly" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />

View file

@ -9,6 +9,7 @@
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Shouldly" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />

View file

@ -13,6 +13,7 @@
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Shouldly" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
@ -21,7 +22,7 @@
<ItemGroup>
<!-- The packages in this ItemGroup are all transitive dependencies that
would otherwise resolve to a version with a security vulnerabilitiy.
would otherwise resolve to a version with a security vulnerability.
In future, we would like to update Microsoft.Data.SqlClient and
Microsoft.EntityFrameworkCore, and remove these explicit dependencies
(assuming that future versions of the intermediate dependencies that
@ -36,4 +37,4 @@
<ProjectReference Include="..\..\src\IdentityServer\Duende.IdentityServer.csproj" />
<ProjectReference Include="..\..\src\Storage\Duende.IdentityServer.Storage.csproj" />
</ItemGroup>
</Project>
</Project>

View file

@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Shouldly" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />

View file

@ -18,6 +18,7 @@
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="All" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="FluentAssertions.Web" />
<PackageReference Include="Shouldly" />
</ItemGroup>
<ItemGroup>

View file

@ -0,0 +1,63 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer.Endpoints.Results;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Shouldly;
using Xunit;
namespace UnitTests.Endpoints.Results;
public class ProtectedResourceErrorResultTests
{
private readonly ProtectedResourceErrorHttpWriter writer = new();
[Fact]
public void WwwAuthenticate_header_with_error_and_description_should_be_a_single_line()
{
var context = new DefaultHttpContext();
writer.WriteHttpResponse(
new ProtectedResourceErrorResult("oops", "big oops"),
context
);
var wwwAuthHeader = context.Response.Headers[HeaderNames.WWWAuthenticate].ToString();
wwwAuthHeader.ShouldBe(
"""
Bearer realm="IdentityServer",error="oops",error_description="big oops"
""");
}
[Fact]
public void WwwAuthenticate_header_with_error_should_be_a_single_line()
{
var context = new DefaultHttpContext();
writer.WriteHttpResponse(
new ProtectedResourceErrorResult("oops"),
context
);
var wwwAuthHeader = context.Response.Headers[HeaderNames.WWWAuthenticate].ToString();
wwwAuthHeader.ShouldBe(
"""
Bearer realm="IdentityServer",error="oops"
""");
}
[Fact]
public void WwwAuthenticate_header_should_always_be_a_single_string_value()
{
var context = new DefaultHttpContext();
writer.WriteHttpResponse(
new ProtectedResourceErrorResult("oops", "big oops"),
context
);
var wwwAuthHeader = context.Response.Headers[HeaderNames.WWWAuthenticate];
wwwAuthHeader.Count.ShouldBe(1);
}
}

View file

@ -16,6 +16,7 @@
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="All" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Shouldly" />
</ItemGroup>
<ItemGroup>