Skip to content
This repository was archived by the owner on Aug 24, 2025. It is now read-only.

Commit 21efde6

Browse files
committed
Add DeviceDetectionService
1 parent 36542f8 commit 21efde6

File tree

9 files changed

+123
-8
lines changed

9 files changed

+123
-8
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ jobs:
1212
- name: Setup .NET Core
1313
uses: actions/setup-dotnet@v1
1414
with:
15-
dotnet-version: 7.0.200
15+
dotnet-version: 7.0.202
1616
- name: Build ASPNETCore2JwtAuthentication
1717
run: dotnet build ./src/ASPNETCore2JwtAuthentication.WebApp/ASPNETCore2JwtAuthentication.WebApp.csproj --configuration Release

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@
2727
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.3" />
2828
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
2929
<PackageVersion Include="Microsoft.Web.LibraryManager.Build" Version="2.1.175" />
30+
<PackageVersion Include="UAParser" Version="3.1.47" />
3031
</ItemGroup>
3132
</Project>

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"sdk": {
3-
"version": "7.0.200"
3+
"version": "7.0.202"
44
}
55
}

src/ASPNETCore2JwtAuthentication.IoCConfig/ConfigureServicesExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ public static void AddCustomDbContext(this IServiceCollection services, IConfigu
152152
public static void AddCustomServices(this IServiceCollection services)
153153
{
154154
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
155+
services.AddScoped<IDeviceDetectionService, DeviceDetectionService>();
155156
services.AddScoped<IAntiForgeryCookieService, AntiForgeryCookieService>();
156157
services.AddScoped<IUnitOfWork, ApplicationDbContext>();
157158
services.AddScoped<IUsersService, UsersService>();

src/ASPNETCore2JwtAuthentication.Services/ASPNETCore2JwtAuthentication.Services.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
<ItemGroup>
1313
<PackageReference Include="Microsoft.EntityFrameworkCore" />
1414
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
15+
<PackageReference Include="UAParser" />
1516
</ItemGroup>
1617
</Project>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System.Security.Claims;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.Net.Http.Headers;
4+
using UAParser;
5+
6+
namespace ASPNETCore2JwtAuthentication.Services;
7+
8+
/// <summary>
9+
/// To invalidate an old user's token from a new device
10+
/// </summary>
11+
public class DeviceDetectionService : IDeviceDetectionService
12+
{
13+
private readonly IHttpContextAccessor _httpContextAccessor;
14+
private readonly ISecurityService _securityService;
15+
16+
public DeviceDetectionService(ISecurityService securityService, IHttpContextAccessor httpContextAccessor)
17+
{
18+
_securityService = securityService ?? throw new ArgumentNullException(nameof(securityService));
19+
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
20+
}
21+
22+
public string GetCurrentRequestDeviceDetails() => GetDeviceDetails(_httpContextAccessor.HttpContext);
23+
24+
public string GetDeviceDetails(HttpContext context)
25+
{
26+
var ua = GetUserAgent(context);
27+
if (ua is null)
28+
{
29+
return "unknown";
30+
}
31+
32+
var client = Parser.GetDefault().Parse(ua);
33+
var deviceInfo = client.Device.Family;
34+
var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}";
35+
var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}";
36+
//TODO: Add the user's IP address here, if it's a banking system.
37+
return $"{deviceInfo}, {browserInfo}, {osInfo}";
38+
}
39+
40+
public string GetDeviceDetailsHash(HttpContext context) =>
41+
_securityService.GetSha256Hash(GetDeviceDetails(context));
42+
43+
public string GetCurrentRequestDeviceDetailsHash() => GetDeviceDetailsHash(_httpContextAccessor.HttpContext);
44+
45+
public string GetCurrentUserTokenDeviceDetailsHash() =>
46+
GetUserTokenDeviceDetailsHash(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity);
47+
48+
public string GetUserTokenDeviceDetailsHash(ClaimsIdentity claimsIdentity)
49+
{
50+
if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
51+
{
52+
return null;
53+
}
54+
55+
return claimsIdentity.FindFirst(ClaimTypes.System)?.Value;
56+
}
57+
58+
public bool HasCurrentUserTokenValidDeviceDetails() =>
59+
HasUserTokenValidDeviceDetails(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity);
60+
61+
public bool HasUserTokenValidDeviceDetails(ClaimsIdentity claimsIdentity) =>
62+
string.Equals(GetCurrentRequestDeviceDetailsHash(), GetUserTokenDeviceDetailsHash(claimsIdentity),
63+
StringComparison.Ordinal);
64+
65+
private static string GetUserAgent(HttpContext context)
66+
{
67+
if (context is null)
68+
{
69+
return null;
70+
}
71+
72+
return context.Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgent)
73+
? userAgent.ToString()
74+
: null;
75+
}
76+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Security.Claims;
2+
using Microsoft.AspNetCore.Http;
3+
4+
namespace ASPNETCore2JwtAuthentication.Services;
5+
6+
public interface IDeviceDetectionService
7+
{
8+
string GetDeviceDetails(HttpContext context);
9+
string GetCurrentRequestDeviceDetails();
10+
11+
string GetDeviceDetailsHash(HttpContext context);
12+
string GetCurrentRequestDeviceDetailsHash();
13+
14+
string GetUserTokenDeviceDetailsHash(ClaimsIdentity claimsIdentity);
15+
string GetCurrentUserTokenDeviceDetailsHash();
16+
17+
bool HasUserTokenValidDeviceDetails(ClaimsIdentity claimsIdentity);
18+
bool HasCurrentUserTokenValidDeviceDetails();
19+
}

src/ASPNETCore2JwtAuthentication.Services/TokenFactoryService.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace ASPNETCore2JwtAuthentication.Services;
1212
public class TokenFactoryService : ITokenFactoryService
1313
{
1414
private readonly IOptionsSnapshot<BearerTokensOptions> _configuration;
15+
private readonly IDeviceDetectionService _deviceDetectionService;
1516
private readonly ILogger<TokenFactoryService> _logger;
1617
private readonly IRolesService _rolesService;
1718
private readonly ISecurityService _securityService;
@@ -20,12 +21,15 @@ public TokenFactoryService(
2021
ISecurityService securityService,
2122
IRolesService rolesService,
2223
IOptionsSnapshot<BearerTokensOptions> configuration,
23-
ILogger<TokenFactoryService> logger)
24+
ILogger<TokenFactoryService> logger,
25+
IDeviceDetectionService deviceDetectionService)
2426
{
2527
_securityService = securityService ?? throw new ArgumentNullException(nameof(securityService));
2628
_rolesService = rolesService ?? throw new ArgumentNullException(nameof(rolesService));
2729
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
2830
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
31+
_deviceDetectionService =
32+
deviceDetectionService ?? throw new ArgumentNullException(nameof(deviceDetectionService));
2933
}
3034

3135
public async Task<JwtTokensData> CreateJwtTokensAsync(User user)
@@ -104,6 +108,8 @@ out _
104108
// for invalidation
105109
new(ClaimTypes.SerialNumber, refreshTokenSerial, ClaimValueTypes.String,
106110
_configuration.Value.Issuer),
111+
new(ClaimTypes.System, _deviceDetectionService.GetCurrentRequestDeviceDetailsHash(),
112+
ClaimValueTypes.String, _configuration.Value.Issuer),
107113
};
108114
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Value.Key));
109115
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
@@ -140,17 +146,17 @@ out _
140146
DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture),
141147
ClaimValueTypes.Integer64, _configuration.Value.Issuer),
142148
new(ClaimTypes.NameIdentifier, user.Id.ToString(CultureInfo.InvariantCulture),
143-
ClaimValueTypes.String,
144-
_configuration.Value.Issuer),
149+
ClaimValueTypes.String, _configuration.Value.Issuer),
145150
new(ClaimTypes.Name, user.Username, ClaimValueTypes.String, _configuration.Value.Issuer),
146151
new("DisplayName", user.DisplayName, ClaimValueTypes.String, _configuration.Value.Issuer),
147152
// to invalidate the cookie
148153
new(ClaimTypes.SerialNumber, user.SerialNumber, ClaimValueTypes.String,
149154
_configuration.Value.Issuer),
150155
// custom data
151156
new(ClaimTypes.UserData, user.Id.ToString(CultureInfo.InvariantCulture),
152-
ClaimValueTypes.String,
153-
_configuration.Value.Issuer),
157+
ClaimValueTypes.String, _configuration.Value.Issuer),
158+
new(ClaimTypes.System, _deviceDetectionService.GetCurrentRequestDeviceDetailsHash(),
159+
ClaimValueTypes.String, _configuration.Value.Issuer),
154160
};
155161

156162
// add roles

src/ASPNETCore2JwtAuthentication.Services/TokenValidatorService.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@ namespace ASPNETCore2JwtAuthentication.Services;
66

77
public class TokenValidatorService : ITokenValidatorService
88
{
9+
private readonly IDeviceDetectionService _deviceDetectionService;
910
private readonly ITokenStoreService _tokenStoreService;
1011
private readonly IUsersService _usersService;
1112

12-
public TokenValidatorService(IUsersService usersService, ITokenStoreService tokenStoreService)
13+
public TokenValidatorService(IUsersService usersService,
14+
ITokenStoreService tokenStoreService,
15+
IDeviceDetectionService deviceDetectionService)
1316
{
1417
_usersService = usersService ?? throw new ArgumentNullException(nameof(usersService));
1518
_tokenStoreService = tokenStoreService ?? throw new ArgumentNullException(nameof(tokenStoreService));
19+
_deviceDetectionService =
20+
deviceDetectionService ?? throw new ArgumentNullException(nameof(deviceDetectionService));
1621
}
1722

1823
public async Task ValidateAsync(TokenValidatedContext context)
@@ -29,6 +34,12 @@ public async Task ValidateAsync(TokenValidatedContext context)
2934
return;
3035
}
3136

37+
if (!_deviceDetectionService.HasUserTokenValidDeviceDetails(claimsIdentity))
38+
{
39+
context.Fail("Detected usage of an old token from a new device! Please login again!");
40+
return;
41+
}
42+
3243
var serialNumberClaim = claimsIdentity.FindFirst(ClaimTypes.SerialNumber);
3344
if (serialNumberClaim == null)
3445
{

0 commit comments

Comments
 (0)