From fcc4985e3e6e3bd9f88af7a8880da2219822c69d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Rosenauer?= <2175979+jorros@users.noreply.github.com> Date: Mon, 18 Jul 2022 14:53:13 +0200 Subject: [PATCH] Add config flag to ignore authenticated status (#5) --- src/AnonymousUser/AnonymousUser.csproj | 1 + src/AnonymousUser/AnonymousUserMiddleware.cs | 34 ++++++++++++------- src/AnonymousUser/AnonymousUserOptions.cs | 5 ++- .../AnonymousUserMiddlewareTests.cs | 19 +++++++---- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/AnonymousUser/AnonymousUser.csproj b/src/AnonymousUser/AnonymousUser.csproj index f3b02ee..2454842 100644 --- a/src/AnonymousUser/AnonymousUser.csproj +++ b/src/AnonymousUser/AnonymousUser.csproj @@ -2,6 +2,7 @@ netstandard2.1 + InsightArchitectures.Extensions.AspNetCore.AnonymousUser diff --git a/src/AnonymousUser/AnonymousUserMiddleware.cs b/src/AnonymousUser/AnonymousUserMiddleware.cs index 0f18b9c..6ff293d 100644 --- a/src/AnonymousUser/AnonymousUserMiddleware.cs +++ b/src/AnonymousUser/AnonymousUserMiddleware.cs @@ -11,8 +11,8 @@ namespace InsightArchitectures.Extensions.AspNetCore.AnonymousUser /// public class AnonymousUserMiddleware { - private RequestDelegate _nextDelegate; - private AnonymousUserOptions _options; + private readonly RequestDelegate _nextDelegate; + private readonly AnonymousUserOptions _options; /// /// Constructor requires the next delegate and options. @@ -25,10 +25,14 @@ public AnonymousUserMiddleware(RequestDelegate nextDelegate, AnonymousUserOption private async Task HandleRequestAsync(HttpContext httpContext) { - var cookieEncoder = _options.EncoderService ?? throw new ArgumentNullException(nameof(_options.EncoderService), $"{nameof(_options.EncoderService)} is null and should have a valid encoder."); - _ = _options.UserIdentifierFactory ?? throw new ArgumentNullException(nameof(_options.UserIdentifierFactory), $"{nameof(_options.UserIdentifierFactory)} is null and should have a valid factory."); + var cookieEncoder = _options.EncoderService ?? throw new ArgumentNullException( + nameof(_options.EncoderService), + $"{nameof(_options.EncoderService)} is null and should have a valid encoder."); + _ = _options.UserIdentifierFactory ?? throw new ArgumentNullException( + nameof(_options.UserIdentifierFactory), + $"{nameof(_options.UserIdentifierFactory)} is null and should have a valid factory."); - if (httpContext.User.Identity?.IsAuthenticated == true) + if (_options.SkipAuthenticated && httpContext.User.Identity?.IsAuthenticated == true) { return; } @@ -52,16 +56,22 @@ private async Task HandleRequestAsync(HttpContext httpContext) uid = _options.UserIdentifierFactory.Invoke(httpContext); var encodedUid = await cookieEncoder.EncodeAsync(uid); - var cookieOptions = new CookieOptions - { - Expires = _options.Expires, - }; + var cookieOptions = new CookieOptions { Expires = _options.Expires }; httpContext.Response.Cookies.Append(_options.CookieName, encodedUid, cookieOptions); } - var identity = new ClaimsIdentity(new[] { new Claim(_options.ClaimType, uid) }); - httpContext.User.AddIdentity(identity); + var claim = new Claim(_options.ClaimType, uid); + + if (httpContext.User.Identity is ClaimsIdentity ci) + { + ci.AddClaim(claim); + } + else + { + var identity = new ClaimsIdentity(new[] { claim }); + httpContext.User.AddIdentity(identity); + } } /// @@ -75,4 +85,4 @@ public async Task InvokeAsync(HttpContext httpContext) await _nextDelegate.Invoke(httpContext); } } -} \ No newline at end of file +} diff --git a/src/AnonymousUser/AnonymousUserOptions.cs b/src/AnonymousUser/AnonymousUserOptions.cs index c874aef..608934c 100644 --- a/src/AnonymousUser/AnonymousUserOptions.cs +++ b/src/AnonymousUser/AnonymousUserOptions.cs @@ -20,10 +20,13 @@ public class AnonymousUserOptions /// Should the cookie only be allowed on https requests. public bool Secure { get; set; } + /// Should the anonymous session id be skipped when an user is authenticated. + public bool SkipAuthenticated { get; set; } + /// Can be overridden to customise the ID generation. public Func UserIdentifierFactory { get; set; } = _ => Guid.NewGuid().ToString(); /// The encoder service to encode/decode the cookie value. Default set to internal base64 encoder. public ICookieEncoder EncoderService { get; set; } = new Base64CookieEncoder(); } -} \ No newline at end of file +} diff --git a/tests/AnonymousUserTests/AnonymousUserMiddlewareTests.cs b/tests/AnonymousUserTests/AnonymousUserMiddlewareTests.cs index 5d75138..22cde6e 100644 --- a/tests/AnonymousUserTests/AnonymousUserMiddlewareTests.cs +++ b/tests/AnonymousUserTests/AnonymousUserMiddlewareTests.cs @@ -12,7 +12,7 @@ namespace AnonymousUserTests { public class AnonymousUserMiddlewareTests { - [Test, CustomAutoDataAttribute] + [Test, CustomAutoData] public async Task NoCookiesShouldCreateCookie(HttpContext context, [Frozen] AnonymousUserOptions options, AnonymousUserMiddleware sut) { await sut.InvokeAsync(context); @@ -22,7 +22,7 @@ public async Task NoCookiesShouldCreateCookie(HttpContext context, [Frozen] Anon Assert.IsFalse(string.IsNullOrWhiteSpace(actual)); } - [Test, CustomAutoDataAttribute] + [Test, CustomAutoData] public async Task ExistingCookieShouldNotAddCookieToResponse(HttpContext context, [Frozen] Mock httpRequest, [Frozen] AnonymousUserOptions options, AnonymousUserMiddleware sut) { var cookies = new Dictionary @@ -38,7 +38,7 @@ public async Task ExistingCookieShouldNotAddCookieToResponse(HttpContext context Assert.IsTrue(string.IsNullOrWhiteSpace(actual)); } - [Test, CustomAutoDataAttribute] + [Test, CustomAutoData] public async Task SecureCookieWithHttpShouldExpire(HttpContext context, [Frozen] Mock httpRequest, [Frozen] AnonymousUserOptions options, AnonymousUserMiddleware sut) { var cookies = new Dictionary @@ -56,15 +56,22 @@ public async Task SecureCookieWithHttpShouldExpire(HttpContext context, [Frozen] Assert.IsEmpty(actual); } - [Test, CustomAutoDataAttribute] + [Test, CustomAutoData] public async Task AuthenticatedUserShouldSkipMiddleware(HttpContext context, [Frozen] Mock claimsPrincipal, AnonymousUserMiddleware sut) { - claimsPrincipal.Setup(x => x.Identity).Returns(new ClaimsIdentity(null, "Test")); + var identityMock = new Mock(() => new ClaimsIdentity(null, "Test")); + claimsPrincipal.Setup(x => x.Identity).Returns(identityMock.Object); + identityMock.Setup(x => x.IsAuthenticated).Returns(true); + claimsPrincipal.Setup(x => x.AddIdentity(It.IsAny())).Verifiable(); + identityMock.Setup(x => x.AddClaim(It.IsAny())).Verifiable(); + + context.User = claimsPrincipal.Object; await sut.InvokeAsync(context); claimsPrincipal.Verify(x => x.AddIdentity(It.IsAny()), Times.Never); + identityMock.Verify(x => x.AddClaim(It.IsAny()), Times.Never); } private string? GetCookieValueFromResponse(HttpResponse response, string cookieName) @@ -87,4 +94,4 @@ public async Task AuthenticatedUserShouldSkipMiddleware(HttpContext context, [Fr return null; } } -} \ No newline at end of file +}