mirror of https://github.com/abpframework/abp.git
committed by
GitHub
8 changed files with 426 additions and 153 deletions
@ -0,0 +1,101 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Antiforgery; |
||||
|
using Microsoft.AspNetCore.Http; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.AntiForgery; |
||||
|
|
||||
|
// Wraps the framework IAntiforgery so the antiforgery token's per-user identifier is computed against a
|
||||
|
// normalized principal on every entry point (generation and validation, controllers and Razor Pages,
|
||||
|
// ABP and built-in filters, cookie and bearer). This keeps the same user consistent across schemes whose
|
||||
|
// user id claim carries a different issuer (e.g. "LOCAL AUTHORITY" for the Identity cookie vs. the token
|
||||
|
// authority for a validated JWT or an OIDC cookie).
|
||||
|
public class AbpAntiforgery : IAntiforgery |
||||
|
{ |
||||
|
protected IAntiforgery Inner { get; } |
||||
|
|
||||
|
protected AbpAntiForgeryOptions Options { get; } |
||||
|
|
||||
|
public AbpAntiforgery( |
||||
|
IAntiforgery inner, |
||||
|
IOptions<AbpAntiForgeryOptions> options) |
||||
|
{ |
||||
|
Inner = inner; |
||||
|
Options = options.Value; |
||||
|
} |
||||
|
|
||||
|
public virtual AntiforgeryTokenSet GetAndStoreTokens(HttpContext httpContext) |
||||
|
{ |
||||
|
return WithNormalizedUser(httpContext, () => Inner.GetAndStoreTokens(httpContext)); |
||||
|
} |
||||
|
|
||||
|
public virtual AntiforgeryTokenSet GetTokens(HttpContext httpContext) |
||||
|
{ |
||||
|
return WithNormalizedUser(httpContext, () => Inner.GetTokens(httpContext)); |
||||
|
} |
||||
|
|
||||
|
public virtual Task<bool> IsRequestValidAsync(HttpContext httpContext) |
||||
|
{ |
||||
|
return WithNormalizedUserAsync(httpContext, () => Inner.IsRequestValidAsync(httpContext)); |
||||
|
} |
||||
|
|
||||
|
public virtual Task ValidateRequestAsync(HttpContext httpContext) |
||||
|
{ |
||||
|
return WithNormalizedUserAsync(httpContext, async () => |
||||
|
{ |
||||
|
await Inner.ValidateRequestAsync(httpContext); |
||||
|
return true; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public virtual void SetCookieTokenAndHeader(HttpContext httpContext) |
||||
|
{ |
||||
|
WithNormalizedUser(httpContext, () => |
||||
|
{ |
||||
|
Inner.SetCookieTokenAndHeader(httpContext); |
||||
|
return true; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected virtual T WithNormalizedUser<T>(HttpContext httpContext, Func<T> action) |
||||
|
{ |
||||
|
if (!Options.NormalizeUserIdClaimIssuer) |
||||
|
{ |
||||
|
return action(); |
||||
|
} |
||||
|
|
||||
|
var normalizer = httpContext.RequestServices.GetRequiredService<IAbpAntiForgeryClaimsPrincipalNormalizer>(); |
||||
|
var originalPrincipal = httpContext.User; |
||||
|
httpContext.User = normalizer.Normalize(originalPrincipal); |
||||
|
try |
||||
|
{ |
||||
|
return action(); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
httpContext.User = originalPrincipal; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<T> WithNormalizedUserAsync<T>(HttpContext httpContext, Func<Task<T>> action) |
||||
|
{ |
||||
|
if (!Options.NormalizeUserIdClaimIssuer) |
||||
|
{ |
||||
|
return await action(); |
||||
|
} |
||||
|
|
||||
|
var normalizer = httpContext.RequestServices.GetRequiredService<IAbpAntiForgeryClaimsPrincipalNormalizer>(); |
||||
|
var originalPrincipal = httpContext.User; |
||||
|
httpContext.User = normalizer.Normalize(originalPrincipal); |
||||
|
try |
||||
|
{ |
||||
|
return await action(); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
httpContext.User = originalPrincipal; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,182 @@ |
|||||
|
using System; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Antiforgery; |
||||
|
using Microsoft.AspNetCore.Http; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Security.Claims; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.AntiForgery; |
||||
|
|
||||
|
public class AbpAntiforgery_Tests |
||||
|
{ |
||||
|
private const string BearerIssuer = "https://localhost:44361/"; |
||||
|
private const string UserId = "3a0e6f1c-1111-2222-3333-444455556666"; |
||||
|
|
||||
|
[Fact] |
||||
|
public Task GetAndStoreTokens_should_normalize_the_user_and_restore_it() => |
||||
|
Should_normalize_then_restore( |
||||
|
(antiforgery, httpContext) => { antiforgery.GetAndStoreTokens(httpContext); return Task.CompletedTask; }, |
||||
|
inner => inner.UserSeenByGetAndStoreTokens); |
||||
|
|
||||
|
[Fact] |
||||
|
public Task GetTokens_should_normalize_the_user_and_restore_it() => |
||||
|
Should_normalize_then_restore( |
||||
|
(antiforgery, httpContext) => { antiforgery.GetTokens(httpContext); return Task.CompletedTask; }, |
||||
|
inner => inner.UserSeenByGetTokens); |
||||
|
|
||||
|
[Fact] |
||||
|
public Task IsRequestValidAsync_should_normalize_the_user_and_restore_it() => |
||||
|
Should_normalize_then_restore( |
||||
|
(antiforgery, httpContext) => antiforgery.IsRequestValidAsync(httpContext), |
||||
|
inner => inner.UserSeenByIsRequestValid); |
||||
|
|
||||
|
[Fact] |
||||
|
public Task ValidateRequestAsync_should_normalize_the_user_and_restore_it() => |
||||
|
Should_normalize_then_restore( |
||||
|
(antiforgery, httpContext) => antiforgery.ValidateRequestAsync(httpContext), |
||||
|
inner => inner.UserSeenByValidateRequest); |
||||
|
|
||||
|
[Fact] |
||||
|
public Task SetCookieTokenAndHeader_should_normalize_the_user_and_restore_it() => |
||||
|
Should_normalize_then_restore( |
||||
|
(antiforgery, httpContext) => { antiforgery.SetCookieTokenAndHeader(httpContext); return Task.CompletedTask; }, |
||||
|
inner => inner.UserSeenBySetCookieTokenAndHeader); |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_delegate_the_result_to_the_inner_antiforgery() |
||||
|
{ |
||||
|
var inner = new RecordingAntiforgery(); |
||||
|
var antiforgery = new AbpAntiforgery(inner, CreateOptions(normalize: true)); |
||||
|
var httpContext = CreateHttpContext(CreatePrincipal(BearerIssuer), withNormalizer: true); |
||||
|
|
||||
|
var tokenSet = antiforgery.GetAndStoreTokens(httpContext); |
||||
|
|
||||
|
tokenSet.RequestToken.ShouldBe(RecordingAntiforgery.RequestToken); |
||||
|
tokenSet.CookieToken.ShouldBe(RecordingAntiforgery.CookieToken); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_normalize_when_disabled() |
||||
|
{ |
||||
|
var inner = new RecordingAntiforgery(); |
||||
|
var antiforgery = new AbpAntiforgery(inner, CreateOptions(normalize: false)); |
||||
|
var original = CreatePrincipal(BearerIssuer); |
||||
|
var httpContext = CreateHttpContext(original, withNormalizer: true); |
||||
|
|
||||
|
antiforgery.GetAndStoreTokens(httpContext); |
||||
|
|
||||
|
// the inner saw the original (un-normalized) principal
|
||||
|
inner.UserSeenByGetAndStoreTokens.ShouldBeSameAs(original); |
||||
|
inner.UserSeenByGetAndStoreTokens!.FindFirst(AbpClaimTypes.UserId)!.Issuer.ShouldBe(BearerIssuer); |
||||
|
httpContext.User.ShouldBeSameAs(original); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_resolve_the_normalizer_service_when_disabled() |
||||
|
{ |
||||
|
// the normalizer is intentionally not registered; the disabled fast-path must not touch RequestServices
|
||||
|
var inner = new RecordingAntiforgery(); |
||||
|
var antiforgery = new AbpAntiforgery(inner, CreateOptions(normalize: false)); |
||||
|
var httpContext = CreateHttpContext(CreatePrincipal(BearerIssuer), withNormalizer: false); |
||||
|
|
||||
|
Should.NotThrow(() => antiforgery.GetAndStoreTokens(httpContext)); |
||||
|
} |
||||
|
|
||||
|
private static async Task Should_normalize_then_restore( |
||||
|
Func<IAntiforgery, HttpContext, Task> invoke, |
||||
|
Func<RecordingAntiforgery, ClaimsPrincipal?> userSeenByInner) |
||||
|
{ |
||||
|
var inner = new RecordingAntiforgery(); |
||||
|
var antiforgery = new AbpAntiforgery(inner, CreateOptions(normalize: true)); |
||||
|
var original = CreatePrincipal(BearerIssuer); |
||||
|
var httpContext = CreateHttpContext(original, withNormalizer: true); |
||||
|
|
||||
|
await invoke(antiforgery, httpContext); |
||||
|
|
||||
|
// the inner ran against the normalized principal
|
||||
|
userSeenByInner(inner)!.FindFirst(AbpClaimTypes.UserId)!.Issuer |
||||
|
.ShouldBe(AbpAntiForgeryClaimsPrincipalNormalizer.UserIdClaimIssuer); |
||||
|
// the original principal is restored after the call
|
||||
|
httpContext.User.ShouldBeSameAs(original); |
||||
|
} |
||||
|
|
||||
|
private static Microsoft.Extensions.Options.IOptions<AbpAntiForgeryOptions> CreateOptions(bool normalize) |
||||
|
{ |
||||
|
return Microsoft.Extensions.Options.Options.Create( |
||||
|
new AbpAntiForgeryOptions { NormalizeUserIdClaimIssuer = normalize }); |
||||
|
} |
||||
|
|
||||
|
private static HttpContext CreateHttpContext(ClaimsPrincipal user, bool withNormalizer) |
||||
|
{ |
||||
|
var services = new ServiceCollection(); |
||||
|
if (withNormalizer) |
||||
|
{ |
||||
|
services.AddTransient<IAbpAntiForgeryClaimsPrincipalNormalizer, AbpAntiForgeryClaimsPrincipalNormalizer>(); |
||||
|
} |
||||
|
|
||||
|
return new DefaultHttpContext |
||||
|
{ |
||||
|
User = user, |
||||
|
RequestServices = services.BuildServiceProvider() |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private static ClaimsPrincipal CreatePrincipal(string userIdClaimIssuer) |
||||
|
{ |
||||
|
return new ClaimsPrincipal(new ClaimsIdentity( |
||||
|
new[] |
||||
|
{ |
||||
|
new Claim(AbpClaimTypes.UserId, UserId, ClaimValueTypes.String, userIdClaimIssuer) |
||||
|
}, |
||||
|
"AuthenticationTypes.Federation")); |
||||
|
} |
||||
|
|
||||
|
private sealed class RecordingAntiforgery : IAntiforgery |
||||
|
{ |
||||
|
public const string RequestToken = "test-request-token"; |
||||
|
public const string CookieToken = "test-cookie-token"; |
||||
|
|
||||
|
public ClaimsPrincipal? UserSeenByGetAndStoreTokens { get; private set; } |
||||
|
public ClaimsPrincipal? UserSeenByGetTokens { get; private set; } |
||||
|
public ClaimsPrincipal? UserSeenByIsRequestValid { get; private set; } |
||||
|
public ClaimsPrincipal? UserSeenByValidateRequest { get; private set; } |
||||
|
public ClaimsPrincipal? UserSeenBySetCookieTokenAndHeader { get; private set; } |
||||
|
|
||||
|
public AntiforgeryTokenSet GetAndStoreTokens(HttpContext httpContext) |
||||
|
{ |
||||
|
UserSeenByGetAndStoreTokens = httpContext.User; |
||||
|
return CreateTokenSet(); |
||||
|
} |
||||
|
|
||||
|
public AntiforgeryTokenSet GetTokens(HttpContext httpContext) |
||||
|
{ |
||||
|
UserSeenByGetTokens = httpContext.User; |
||||
|
return CreateTokenSet(); |
||||
|
} |
||||
|
|
||||
|
public Task<bool> IsRequestValidAsync(HttpContext httpContext) |
||||
|
{ |
||||
|
UserSeenByIsRequestValid = httpContext.User; |
||||
|
return Task.FromResult(true); |
||||
|
} |
||||
|
|
||||
|
public Task ValidateRequestAsync(HttpContext httpContext) |
||||
|
{ |
||||
|
UserSeenByValidateRequest = httpContext.User; |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
public void SetCookieTokenAndHeader(HttpContext httpContext) |
||||
|
{ |
||||
|
UserSeenBySetCookieTokenAndHeader = httpContext.User; |
||||
|
} |
||||
|
|
||||
|
private static AntiforgeryTokenSet CreateTokenSet() |
||||
|
{ |
||||
|
return new AntiforgeryTokenSet(RequestToken, CookieToken, "RequestVerificationToken", "RequestVerificationToken"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue