Skip to content

Commit d5ba969

Browse files
authored
Add anonymous user middleware package (#2)
1 parent dd33f17 commit d5ba969

15 files changed

+534
-1
lines changed

.vscode/launch.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
// Use IntelliSense to find out which attributes exist for C# debugging
6+
// Use hover for the description of the existing attributes
7+
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
8+
"name": ".NET Core Launch (console)",
9+
"type": "coreclr",
10+
"request": "launch",
11+
"preLaunchTask": "build",
12+
// If you have changed target frameworks, make sure to update the program path.
13+
"program": "${workspaceFolder}/tests/AnonymousUserTests/bin/Debug/net6.0/AnonymousUserTests.dll",
14+
"args": [],
15+
"cwd": "${workspaceFolder}/tests/AnonymousUserTests",
16+
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
17+
"console": "internalConsole",
18+
"stopAtEntry": false
19+
},
20+
{
21+
"name": ".NET Core Attach",
22+
"type": "coreclr",
23+
"request": "attach"
24+
}
25+
]
26+
}

.vscode/tasks.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "build",
6+
"command": "dotnet",
7+
"type": "process",
8+
"args": [
9+
"build",
10+
"${workspaceFolder}/tests/AnonymousUserTests/AnonymousUserTests.csproj",
11+
"/property:GenerateFullPaths=true",
12+
"/consoleloggerparameters:NoSummary"
13+
],
14+
"problemMatcher": "$msCompile"
15+
},
16+
{
17+
"label": "publish",
18+
"command": "dotnet",
19+
"type": "process",
20+
"args": [
21+
"publish",
22+
"${workspaceFolder}/tests/AnonymousUserTests/AnonymousUserTests.csproj",
23+
"/property:GenerateFullPaths=true",
24+
"/consoleloggerparameters:NoSummary"
25+
],
26+
"problemMatcher": "$msCompile"
27+
},
28+
{
29+
"label": "watch",
30+
"command": "dotnet",
31+
"type": "process",
32+
"args": [
33+
"watch",
34+
"run",
35+
"--project",
36+
"${workspaceFolder}/tests/AnonymousUserTests/AnonymousUserTests.csproj"
37+
],
38+
"problemMatcher": "$msCompile"
39+
}
40+
]
41+
}

AspNetCoreExtensions.sln

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 16
44
VisualStudioVersion = 16.6.30114.105
55
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{38DD8F4A-2C3A-486C-AE3A-7208007EB1B5}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnonymousUser", "src\AnonymousUser\AnonymousUser.csproj", "{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}"
9+
EndProject
10+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9CEDA0A6-C7E0-4542-896C-241C3D796D89}"
11+
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnonymousUserTests", "tests\AnonymousUserTests\AnonymousUserTests.csproj", "{82E9B456-9918-4499-A165-A30423BA0740}"
13+
EndProject
614
Global
715
GlobalSection(SolutionConfigurationPlatforms) = preSolution
816
Debug|Any CPU = Debug|Any CPU
@@ -15,4 +23,34 @@ Global
1523
GlobalSection(SolutionProperties) = preSolution
1624
HideSolutionNode = FALSE
1725
EndGlobalSection
26+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
27+
{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28+
{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|Any CPU.Build.0 = Debug|Any CPU
29+
{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|x64.ActiveCfg = Debug|Any CPU
30+
{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|x64.Build.0 = Debug|Any CPU
31+
{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|x86.ActiveCfg = Debug|Any CPU
32+
{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|x86.Build.0 = Debug|Any CPU
33+
{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|Any CPU.ActiveCfg = Release|Any CPU
34+
{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|Any CPU.Build.0 = Release|Any CPU
35+
{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|x64.ActiveCfg = Release|Any CPU
36+
{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|x64.Build.0 = Release|Any CPU
37+
{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|x86.ActiveCfg = Release|Any CPU
38+
{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|x86.Build.0 = Release|Any CPU
39+
{82E9B456-9918-4499-A165-A30423BA0740}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
40+
{82E9B456-9918-4499-A165-A30423BA0740}.Debug|Any CPU.Build.0 = Debug|Any CPU
41+
{82E9B456-9918-4499-A165-A30423BA0740}.Debug|x64.ActiveCfg = Debug|Any CPU
42+
{82E9B456-9918-4499-A165-A30423BA0740}.Debug|x64.Build.0 = Debug|Any CPU
43+
{82E9B456-9918-4499-A165-A30423BA0740}.Debug|x86.ActiveCfg = Debug|Any CPU
44+
{82E9B456-9918-4499-A165-A30423BA0740}.Debug|x86.Build.0 = Debug|Any CPU
45+
{82E9B456-9918-4499-A165-A30423BA0740}.Release|Any CPU.ActiveCfg = Release|Any CPU
46+
{82E9B456-9918-4499-A165-A30423BA0740}.Release|Any CPU.Build.0 = Release|Any CPU
47+
{82E9B456-9918-4499-A165-A30423BA0740}.Release|x64.ActiveCfg = Release|Any CPU
48+
{82E9B456-9918-4499-A165-A30423BA0740}.Release|x64.Build.0 = Release|Any CPU
49+
{82E9B456-9918-4499-A165-A30423BA0740}.Release|x86.ActiveCfg = Release|Any CPU
50+
{82E9B456-9918-4499-A165-A30423BA0740}.Release|x86.Build.0 = Release|Any CPU
51+
EndGlobalSection
52+
GlobalSection(NestedProjects) = preSolution
53+
{9FB11E67-7313-4BC7-9412-3FF4CAF35C96} = {38DD8F4A-2C3A-486C-AE3A-7208007EB1B5}
54+
{82E9B456-9918-4499-A165-A30423BA0740} = {9CEDA0A6-C7E0-4542-896C-241C3D796D89}
55+
EndGlobalSection
1856
EndGlobal

CodeAnalysis.Library.ruleset

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<Rule Id="CA1006" Action="None" />
66
<Rule Id="CA1303" Action="None" />
77
<Rule Id="CA2243" Action="None" />
8+
<Rule Id="CA2007" Action="None" />
89
</Rules>
910
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
1011
<Rule Id="SA1101" Action="None" /> <!-- Prefix local calls with this -->

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ max_jobs: 1
33
environment:
44
CAKE_SETTINGS_SKIPPACKAGEVERSIONCHECK: "true"
55

6-
image: Visual Studio 2019
6+
image: Visual Studio 2022
77

88
cache:
99
- '%LocalAppData%\NuGet\v3-cache'
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.1</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
9+
</ItemGroup>
10+
11+
</Project>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using InsightArchitectures.Extensions.AspNetCore.AnonymousUser;
3+
4+
namespace Microsoft.AspNetCore.Builder
5+
{
6+
/// <summary>
7+
/// Extension methods for the ASP NET Core application builder.
8+
/// </summary>
9+
public static class AnonymousUserExtensions
10+
{
11+
/// <summary>
12+
/// Adds the <see cref="AnonymousUserMiddleware" /> to the middleware pipeline.
13+
/// </summary>
14+
/// <param name="builder">The application builder object.</param>
15+
/// <param name="configure">An action to customise the middleware options.</param>
16+
public static IApplicationBuilder UseAnonymousUser(this IApplicationBuilder builder, Action<AnonymousUserOptions> configure = null)
17+
{
18+
var options = new AnonymousUserOptions();
19+
20+
configure?.Invoke(options);
21+
22+
_ = options.ClaimType ?? throw new NullReferenceException($"{nameof(options.ClaimType)} is null. Please provide a claim type name when configuring the middleware.");
23+
24+
return builder.UseMiddleware<AnonymousUserMiddleware>(options);
25+
}
26+
}
27+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System;
2+
using System.Security.Claims;
3+
using System.Threading.Tasks;
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace InsightArchitectures.Extensions.AspNetCore.AnonymousUser
7+
{
8+
/// <summary>
9+
/// The anonymous user middleware. It either creates a new or reads an existing cookie
10+
/// and maps the value to a claim.
11+
/// </summary>
12+
public class AnonymousUserMiddleware
13+
{
14+
private RequestDelegate _nextDelegate;
15+
private AnonymousUserOptions _options;
16+
17+
/// <summary>
18+
/// Constructor requires the next delegate and options.
19+
/// </summary>
20+
public AnonymousUserMiddleware(RequestDelegate nextDelegate, AnonymousUserOptions options)
21+
{
22+
_nextDelegate = nextDelegate ?? throw new ArgumentNullException(nameof(nextDelegate));
23+
_options = options ?? throw new ArgumentNullException(nameof(options));
24+
}
25+
26+
private async Task HandleRequestAsync(HttpContext httpContext)
27+
{
28+
var cookieEncoder = _options.EncoderService ?? throw new ArgumentNullException(nameof(_options.EncoderService), $"{nameof(_options.EncoderService)} is null and should have a valid encoder.");
29+
_ = _options.UserIdentifierFactory ?? throw new ArgumentNullException(nameof(_options.UserIdentifierFactory), $"{nameof(_options.UserIdentifierFactory)} is null and should have a valid factory.");
30+
31+
if (httpContext.User.Identity?.IsAuthenticated == true)
32+
{
33+
return;
34+
}
35+
36+
var encodedValue = httpContext.Request.Cookies[_options.CookieName];
37+
38+
if (_options.Secure && !httpContext.Request.IsHttps)
39+
{
40+
if (!string.IsNullOrWhiteSpace(encodedValue))
41+
{
42+
httpContext.Response.Cookies.Delete(_options.CookieName);
43+
}
44+
45+
return;
46+
}
47+
48+
var uid = await cookieEncoder.DecodeAsync(encodedValue);
49+
50+
if (string.IsNullOrWhiteSpace(uid))
51+
{
52+
uid = _options.UserIdentifierFactory.Invoke(httpContext);
53+
var encodedUid = await cookieEncoder.EncodeAsync(uid);
54+
55+
var cookieOptions = new CookieOptions
56+
{
57+
Expires = _options.Expires,
58+
};
59+
60+
httpContext.Response.Cookies.Append(_options.CookieName, encodedUid, cookieOptions);
61+
}
62+
63+
var identity = new ClaimsIdentity(new[] { new Claim(_options.ClaimType, uid) });
64+
httpContext.User.AddIdentity(identity);
65+
}
66+
67+
/// <summary>
68+
/// Called by the pipeline, runs the handler.
69+
/// </summary>
70+
public async Task InvokeAsync(HttpContext httpContext)
71+
{
72+
_ = httpContext ?? throw new ArgumentNullException(nameof(httpContext));
73+
await HandleRequestAsync(httpContext);
74+
75+
await _nextDelegate.Invoke(httpContext);
76+
}
77+
}
78+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
using Microsoft.AspNetCore.Http;
3+
4+
namespace InsightArchitectures.Extensions.AspNetCore.AnonymousUser
5+
{
6+
/// <summary>
7+
/// Configuration options for the middleware.
8+
/// </summary>
9+
public class AnonymousUserOptions
10+
{
11+
/// <summary>The name of the cookie.</summary>
12+
public string CookieName { get; set; } = "tid";
13+
14+
/// <summary>The expiration date of the cookie. Default set to 10 years.</summary>
15+
public DateTimeOffset Expires { get; set; } = DateTimeOffset.UtcNow.AddDays(3652);
16+
17+
/// <summary>The type name of the claim holding the ID.</summary>
18+
public string ClaimType { get; set; } = "ExternalId";
19+
20+
/// <summary>Should the cookie only be allowed on https requests.</summary>
21+
public bool Secure { get; set; }
22+
23+
/// <summary>Can be overridden to customise the ID generation.</summary>
24+
public Func<HttpContext, string> UserIdentifierFactory { get; set; } = _ => Guid.NewGuid().ToString();
25+
26+
/// <summary>The encoder service to encode/decode the cookie value. Default set to internal base64 encoder.</summary>
27+
public ICookieEncoder EncoderService { get; set; } = new Base64CookieEncoder();
28+
}
29+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System;
2+
using System.Text;
3+
using System.Threading.Tasks;
4+
5+
namespace InsightArchitectures.Extensions.AspNetCore.AnonymousUser
6+
{
7+
/// <summary>
8+
/// Default cookie value encoder/decoder. Uses base64 for serialisation.
9+
/// </summary>
10+
public class Base64CookieEncoder : ICookieEncoder
11+
{
12+
/// <summary>
13+
/// Deserialises a base64 value into clear text.
14+
/// <param name="encodedValue">A base64 encoded value.</param>
15+
/// <returns>Returns null if <paramref name="encodedValue" /> is null, otherwise the decoded value.</returns>
16+
/// </summary>
17+
public Task<string> DecodeAsync(string encodedValue)
18+
{
19+
if (string.IsNullOrWhiteSpace(encodedValue))
20+
{
21+
return Task.FromResult((string)null);
22+
}
23+
24+
var bytes = Convert.FromBase64String(encodedValue);
25+
26+
return Task.FromResult(Encoding.UTF8.GetString(bytes));
27+
}
28+
29+
/// <summary>
30+
/// Serialiases a clear text value into base64.
31+
/// <param name="value">A clear text value.</param>
32+
/// <returns>Returns null if <paramref name="value" /> is null, otherwise the encoded value.</returns>
33+
/// </summary>
34+
public Task<string> EncodeAsync(string value)
35+
{
36+
if (string.IsNullOrWhiteSpace(value))
37+
{
38+
return Task.FromResult((string)null);
39+
}
40+
41+
var bytes = Encoding.UTF8.GetBytes(value);
42+
43+
return Task.FromResult(Convert.ToBase64String(bytes));
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)