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
+}