Skip to content

Commit 8f57ad2

Browse files
Implement PersonalDataProtectionService and enable personal data protection
Co-authored-by: BenjaminMichaelis <[email protected]>
1 parent b0ba3d2 commit 8f57ad2

File tree

4 files changed

+275
-23
lines changed

4 files changed

+275
-23
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using EssentialCSharp.Web.Areas.Identity.Data;
2+
using EssentialCSharp.Web.Services;
3+
using Microsoft.AspNetCore.DataProtection;
4+
using Microsoft.AspNetCore.Identity;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Xunit;
7+
8+
namespace EssentialCSharp.Web.Tests;
9+
10+
/// <summary>
11+
/// Integration tests for Personal Data Protection functionality with Identity User
12+
/// </summary>
13+
public class PersonalDataProtectionIntegrationTests
14+
{
15+
[Fact]
16+
public void PersonalDataProtectionService_ImplementsIPersonalDataProtector()
17+
{
18+
// Arrange
19+
var services = new ServiceCollection();
20+
services.AddDataProtection();
21+
var serviceProvider = services.BuildServiceProvider();
22+
var dataProtectionProvider = serviceProvider.GetRequiredService<IDataProtectionProvider>();
23+
24+
// Act
25+
var service = new PersonalDataProtectionService(dataProtectionProvider);
26+
27+
// Assert
28+
Assert.IsAssignableFrom<IPersonalDataProtector>(service);
29+
}
30+
31+
[Fact]
32+
public void EssentialCSharpWebUser_HasProtectedPersonalDataAttributes()
33+
{
34+
// Arrange & Act
35+
var user = new EssentialCSharpWebUser();
36+
var firstNameProperty = typeof(EssentialCSharpWebUser).GetProperty(nameof(EssentialCSharpWebUser.FirstName));
37+
var lastNameProperty = typeof(EssentialCSharpWebUser).GetProperty(nameof(EssentialCSharpWebUser.LastName));
38+
39+
// Assert
40+
Assert.NotNull(firstNameProperty);
41+
Assert.NotNull(lastNameProperty);
42+
43+
var firstNameAttributes = firstNameProperty.GetCustomAttributes(typeof(ProtectedPersonalDataAttribute), false);
44+
var lastNameAttributes = lastNameProperty.GetCustomAttributes(typeof(ProtectedPersonalDataAttribute), false);
45+
46+
Assert.NotEmpty(firstNameAttributes);
47+
Assert.NotEmpty(lastNameAttributes);
48+
}
49+
50+
[Fact]
51+
public void PersonalDataProtectionService_CanProtectUserPersonalData()
52+
{
53+
// Arrange
54+
var services = new ServiceCollection();
55+
services.AddDataProtection();
56+
var serviceProvider = services.BuildServiceProvider();
57+
var dataProtectionProvider = serviceProvider.GetRequiredService<IDataProtectionProvider>();
58+
var service = new PersonalDataProtectionService(dataProtectionProvider);
59+
60+
var testFirstName = "John";
61+
var testLastName = "Doe";
62+
63+
// Act
64+
var protectedFirstName = service.Protect(testFirstName);
65+
var protectedLastName = service.Protect(testLastName);
66+
67+
var unprotectedFirstName = service.Unprotect(protectedFirstName);
68+
var unprotectedLastName = service.Unprotect(protectedLastName);
69+
70+
// Assert
71+
Assert.NotEqual(testFirstName, protectedFirstName);
72+
Assert.NotEqual(testLastName, protectedLastName);
73+
Assert.Equal(testFirstName, unprotectedFirstName);
74+
Assert.Equal(testLastName, unprotectedLastName);
75+
}
76+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using EssentialCSharp.Web.Services;
2+
using Microsoft.AspNetCore.DataProtection;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Logging.Abstractions;
5+
using Xunit;
6+
7+
namespace EssentialCSharp.Web.Tests;
8+
9+
public class PersonalDataProtectionServiceTests
10+
{
11+
private PersonalDataProtectionService CreateService()
12+
{
13+
var services = new ServiceCollection();
14+
services.AddDataProtection();
15+
var serviceProvider = services.BuildServiceProvider();
16+
var dataProtectionProvider = serviceProvider.GetRequiredService<IDataProtectionProvider>();
17+
18+
return new PersonalDataProtectionService(dataProtectionProvider);
19+
}
20+
21+
[Fact]
22+
public void Protect_WithValidData_ReturnsEncryptedString()
23+
{
24+
// Arrange
25+
var service = CreateService();
26+
var testData = "John Doe";
27+
28+
// Act
29+
var protectedData = service.Protect(testData);
30+
31+
// Assert
32+
Assert.NotNull(protectedData);
33+
Assert.NotEmpty(protectedData);
34+
Assert.NotEqual(testData, protectedData);
35+
}
36+
37+
[Fact]
38+
public void Unprotect_WithProtectedData_ReturnsOriginalString()
39+
{
40+
// Arrange
41+
var service = CreateService();
42+
var testData = "Jane Smith";
43+
44+
// Act
45+
var protectedData = service.Protect(testData);
46+
var unprotectedData = service.Unprotect(protectedData);
47+
48+
// Assert
49+
Assert.Equal(testData, unprotectedData);
50+
}
51+
52+
[Theory]
53+
[InlineData(null)]
54+
[InlineData("")]
55+
public void Protect_WithNullOrEmptyData_ReturnsEmptyString(string? testData)
56+
{
57+
// Arrange
58+
var service = CreateService();
59+
60+
// Act
61+
var result = service.Protect(testData);
62+
63+
// Assert
64+
Assert.Equal(string.Empty, result);
65+
}
66+
67+
[Theory]
68+
[InlineData(null)]
69+
[InlineData("")]
70+
public void Unprotect_WithNullOrEmptyData_ReturnsEmptyString(string? testData)
71+
{
72+
// Arrange
73+
var service = CreateService();
74+
75+
// Act
76+
var result = service.Unprotect(testData);
77+
78+
// Assert
79+
Assert.Equal(string.Empty, result);
80+
}
81+
82+
[Fact]
83+
public void Unprotect_WithUnencryptedData_ReturnsOriginalDataForBackwardCompatibility()
84+
{
85+
// Arrange
86+
var service = CreateService();
87+
var unencryptedData = "This is plain text data";
88+
89+
// Act
90+
var result = service.Unprotect(unencryptedData);
91+
92+
// Assert
93+
// Should return the original data when decryption fails (backward compatibility)
94+
Assert.Equal(unencryptedData, result);
95+
}
96+
97+
[Fact]
98+
public void ProtectAndUnprotect_WithSpecialCharacters_WorksCorrectly()
99+
{
100+
// Arrange
101+
var service = CreateService();
102+
var testData = "Special chars: éñüñëç@#$%^&*()";
103+
104+
// Act
105+
var protectedData = service.Protect(testData);
106+
var unprotectedData = service.Unprotect(protectedData);
107+
108+
// Assert
109+
Assert.Equal(testData, unprotectedData);
110+
Assert.NotEqual(testData, protectedData);
111+
}
112+
}

EssentialCSharp.Web/Program.cs

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -65,30 +65,37 @@ private static void Main(string[] args)
6565
}
6666
}
6767

68-
builder.Services.AddDbContext<EssentialCSharpWebContext>(options => options.UseSqlServer(connectionString));
69-
builder.Services.AddDefaultIdentity<EssentialCSharpWebUser>(options =>
70-
{
71-
// Password settings
72-
options.User.RequireUniqueEmail = true;
73-
options.Password.RequiredLength = PasswordRequirementOptions.PasswordMinimumLength;
74-
options.Password.RequireDigit = PasswordRequirementOptions.RequireDigit;
75-
options.Password.RequireNonAlphanumeric = PasswordRequirementOptions.RequireNonAlphanumeric;
76-
options.Password.RequireUppercase = PasswordRequirementOptions.RequireUppercase;
77-
options.Password.RequireLowercase = PasswordRequirementOptions.RequireLowercase;
78-
options.Password.RequiredUniqueChars = PasswordRequirementOptions.RequiredUniqueChars;
79-
80-
options.SignIn.RequireConfirmedEmail = true;
81-
options.SignIn.RequireConfirmedAccount = true;
82-
83-
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
84-
options.Lockout.MaxFailedAccessAttempts = 3;
85-
86-
//TODO: Implement IProtectedUserStore
87-
//options.Stores.ProtectPersonalData = true;
68+
builder.Services.AddDbContext<EssentialCSharpWebContext>(options => options.UseSqlServer(connectionString));
69+
70+
// Add Data Protection services
71+
builder.Services.AddDataProtection();
72+
73+
builder.Services.AddDefaultIdentity<EssentialCSharpWebUser>(options =>
74+
{
75+
// Password settings
76+
options.User.RequireUniqueEmail = true;
77+
options.Password.RequiredLength = PasswordRequirementOptions.PasswordMinimumLength;
78+
options.Password.RequireDigit = PasswordRequirementOptions.RequireDigit;
79+
options.Password.RequireNonAlphanumeric = PasswordRequirementOptions.RequireNonAlphanumeric;
80+
options.Password.RequireUppercase = PasswordRequirementOptions.RequireUppercase;
81+
options.Password.RequireLowercase = PasswordRequirementOptions.RequireLowercase;
82+
options.Password.RequiredUniqueChars = PasswordRequirementOptions.RequiredUniqueChars;
83+
84+
options.SignIn.RequireConfirmedEmail = true;
85+
options.SignIn.RequireConfirmedAccount = true;
86+
87+
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
88+
options.Lockout.MaxFailedAccessAttempts = 3;
89+
90+
// Enable personal data protection for properties marked with [ProtectedPersonalData]
91+
options.Stores.ProtectPersonalData = true;
8892
})
89-
.AddEntityFrameworkStores<EssentialCSharpWebContext>()
90-
.AddPasswordValidator<UsernameOrEmailAsPasswordValidator<EssentialCSharpWebUser>>()
91-
.AddPasswordValidator<Top100000PasswordValidator<EssentialCSharpWebUser>>();
93+
.AddEntityFrameworkStores<EssentialCSharpWebContext>()
94+
.AddPasswordValidator<UsernameOrEmailAsPasswordValidator<EssentialCSharpWebUser>>()
95+
.AddPasswordValidator<Top100000PasswordValidator<EssentialCSharpWebUser>>();
96+
97+
// Register personal data protector for IProtectedUserStore functionality
98+
builder.Services.AddScoped<IPersonalDataProtector, PersonalDataProtectionService>();
9299

93100
builder.Configuration
94101
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using Microsoft.AspNetCore.DataProtection;
2+
using Microsoft.AspNetCore.Identity;
3+
4+
namespace EssentialCSharp.Web.Services;
5+
6+
/// <summary>
7+
/// Service for protecting and unprotecting personal data in the Identity user store
8+
/// using ASP.NET Core Data Protection API.
9+
/// </summary>
10+
public class PersonalDataProtectionService : IPersonalDataProtector
11+
{
12+
private readonly IDataProtector _protector;
13+
14+
public PersonalDataProtectionService(IDataProtectionProvider provider)
15+
{
16+
_protector = provider.CreateProtector("Microsoft.AspNetCore.Identity.PersonalData");
17+
}
18+
19+
/// <summary>
20+
/// Protects (encrypts) the given personal data value.
21+
/// </summary>
22+
/// <param name="data">The data to protect</param>
23+
/// <returns>The protected (encrypted) data as a string</returns>
24+
public string Protect(string? data)
25+
{
26+
if (string.IsNullOrEmpty(data))
27+
{
28+
return string.Empty;
29+
}
30+
31+
return _protector.Protect(data);
32+
}
33+
34+
/// <summary>
35+
/// Unprotects (decrypts) the given protected personal data value.
36+
/// </summary>
37+
/// <param name="data">The protected data to unprotect</param>
38+
/// <returns>The unprotected (decrypted) data as a string</returns>
39+
public string Unprotect(string? data)
40+
{
41+
if (string.IsNullOrEmpty(data))
42+
{
43+
return string.Empty;
44+
}
45+
46+
try
47+
{
48+
return _protector.Unprotect(data);
49+
}
50+
catch (Exception)
51+
{
52+
// If decryption fails, assume the data is not encrypted (for backward compatibility)
53+
// This handles cases where existing user data was stored before encryption was enabled
54+
return data;
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)