Skip to content

Commit 545d786

Browse files
CR feedback plus a test fix
1 parent ac96696 commit 545d786

File tree

4 files changed

+52
-3
lines changed

4 files changed

+52
-3
lines changed

src/Components/Endpoints/src/Builder/OpaqueRedirection.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Security.Cryptography;
45
using System.Text.Encodings.Web;
56
using Microsoft.AspNetCore.Builder;
67
using Microsoft.AspNetCore.DataProtection;
78
using Microsoft.AspNetCore.Http;
89
using Microsoft.AspNetCore.Routing;
910
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Logging;
12+
using Microsoft.Extensions.Options;
1013

1114
namespace Microsoft.AspNetCore.Components.Endpoints;
1215

13-
internal static class OpaqueRedirection
16+
internal partial class OpaqueRedirection
1417
{
1518
// During streaming SSR, a component may try to perform a redirection. Since the response has already started
1619
// this can only work if we communicate the redirection back via some command that can get handled by JS,
@@ -38,7 +41,9 @@ internal static class OpaqueRedirection
3841
public static string CreateProtectedRedirectionUrl(HttpContext httpContext, string destinationUrl)
3942
{
4043
var protector = CreateProtector(httpContext);
41-
var protectedUrl = protector.Protect(destinationUrl, TimeSpan.FromSeconds(10));
44+
var options = httpContext.RequestServices.GetRequiredService<IOptions<RazorComponentsServiceOptions>>();
45+
var lifetime = options.Value.TemporaryRedirectionUrlValidityDuration;
46+
var protectedUrl = protector.Protect(destinationUrl, lifetime);
4247
return $"{RedirectionEndpointBaseRelativeUrl}?url={UrlEncoder.Default.Encode(protectedUrl)}";
4348
}
4449

@@ -53,7 +58,23 @@ public static void AddBlazorOpaqueRedirectionEndpoint(IEndpointRouteBuilder endp
5358
}
5459

5560
var protector = CreateProtector(httpContext);
56-
var url = protector.Unprotect(protectedUrl[0]!);
61+
string url;
62+
63+
try
64+
{
65+
url = protector.Unprotect(protectedUrl[0]!);
66+
}
67+
catch (CryptographicException ex)
68+
{
69+
if (httpContext.RequestServices.GetService<ILogger<OpaqueRedirection>>() is { } logger)
70+
{
71+
Log.OpaqueUrlUnprotectionFailed(logger, ex);
72+
}
73+
74+
httpContext.Response.StatusCode = 400;
75+
return Task.CompletedTask;
76+
}
77+
5778
httpContext.Response.Redirect(url);
5879
return Task.CompletedTask;
5980
});
@@ -64,4 +85,10 @@ private static ITimeLimitedDataProtector CreateProtector(HttpContext httpContext
6485
var dataProtectionProvider = httpContext.RequestServices.GetRequiredService<IDataProtectionProvider>();
6586
return dataProtectionProvider.CreateProtector(RedirectionDataProtectionProviderPurpose).ToTimeLimitedDataProtector();
6687
}
88+
89+
public static partial class Log
90+
{
91+
[LoggerMessage(1, LogLevel.Information, "Opaque URL unprotection failed.", EventName = "OpaqueUrlUnprotectionFailed")]
92+
public static partial void OpaqueUrlUnprotectionFailed(ILogger<OpaqueRedirection> logger, Exception exception);
93+
}
6794
}

src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceOptions.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ namespace Microsoft.AspNetCore.Components.Endpoints;
1010
/// </summary>
1111
public sealed class RazorComponentsServiceOptions
1212
{
13+
// Fairly long default lifetime to allow for clock skew across servers
14+
private TimeSpan _temporaryRedirectionUrlValidityDuration = TimeSpan.FromMinutes(5);
15+
1316
internal FormDataMapperOptions _formMappingOptions = new();
1417

1518
/// <summary>
@@ -63,4 +66,20 @@ public int MaxFormMappingKeySize
6366
get => _formMappingOptions.MaxKeyBufferSize;
6467
set => _formMappingOptions.MaxKeyBufferSize = value;
6568
}
69+
70+
/// <summary>
71+
/// Gets or sets the lifetime of data protection validity for temporary redirection URLs
72+
/// emitted by Blazor server-side rendering. These are only used transiently so the lifetime
73+
/// only needs to be long enough for a client to receive the URL and begin navigation to it.
74+
/// However, it should also be long enough to allow for clock skew across servers.
75+
/// </summary>
76+
public TimeSpan TemporaryRedirectionUrlValidityDuration
77+
{
78+
get => _temporaryRedirectionUrlValidityDuration;
79+
set
80+
{
81+
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(value.TotalMilliseconds, 0);
82+
_temporaryRedirectionUrlValidityDuration = value;
83+
}
84+
}
6685
}

src/Components/Endpoints/src/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.MaxFormM
3030
Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.MaxFormMappingRecursionDepth.get -> int
3131
Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.MaxFormMappingRecursionDepth.set -> void
3232
Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.RazorComponentsServiceOptions() -> void
33+
Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.TemporaryRedirectionUrlValidityDuration.get -> System.TimeSpan
34+
Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.TemporaryRedirectionUrlValidityDuration.set -> void
3335
Microsoft.AspNetCore.Components.Endpoints.RootComponentMetadata
3436
Microsoft.AspNetCore.Components.Endpoints.RootComponentMetadata.RootComponentMetadata(System.Type! rootComponentType) -> void
3537
Microsoft.AspNetCore.Components.Endpoints.RootComponentMetadata.Type.get -> System.Type!

src/Components/Endpoints/test/RazorComponentResultTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ public async Task StreamingRendering_IsOffByDefault_AndCanBeEnabledForSubtree()
347347
// Act/Assert: Produce initial output, noting absence of streaming markers at top level
348348
testContext.TopLevelComponentTask.SetResult();
349349
await initialOutputTask;
350+
await WaitForContentWrittenAsync(testContext.ResponseBody);
350351
var html = MaskComponentIds(GetStringContent(testContext.ResponseBody));
351352
Assert.StartsWith("[Top level component: Loaded]", html);
352353
Assert.Contains("[Within streaming region: <!--bl:X-->Loading...<!--/bl:X-->]", html);

0 commit comments

Comments
 (0)