source: FarmatikoServices/Infrastructure/JwtAuthManager.cs@ d23bf72

Last change on this file since d23bf72 was d23bf72, checked in by DimitarSlezenkovski <dslezenkovski@…>, 2 years ago

Add SystemService, Auth, fix a lil bugs :)

  • Property mode set to 100644
File size: 6.0 KB
Line 
1using System;
2using System.Collections.Concurrent;
3using System.Collections.Immutable;
4using System.IdentityModel.Tokens.Jwt;
5using System.Linq;
6using System.Security.Claims;
7using System.Security.Cryptography;
8using System.Text;
9using System.Text.Json.Serialization;
10using FarmatikoServices.Infrastructure;
11using Microsoft.IdentityModel.Tokens;
12
13namespace FarmatikoServices.Infrastructure
14{
15
16
17 public class JwtAuthManager : IJwtAuthManager
18 {
19 public IImmutableDictionary<string, RefreshToken> UsersRefreshTokensReadOnlyDictionary => _usersRefreshTokens.ToImmutableDictionary();
20 private readonly ConcurrentDictionary<string, RefreshToken> _usersRefreshTokens; // can store in a database or a distributed cache
21 private readonly JwtTokenConfig _jwtTokenConfig;
22 private readonly byte[] _secret;
23
24 public JwtAuthManager(JwtTokenConfig jwtTokenConfig)
25 {
26 _jwtTokenConfig = jwtTokenConfig;
27 _usersRefreshTokens = new ConcurrentDictionary<string, RefreshToken>();
28 _secret = Encoding.ASCII.GetBytes(jwtTokenConfig.Secret);
29 }
30
31 // optional: clean up expired refresh tokens
32 public void RemoveExpiredRefreshTokens(DateTime now)
33 {
34 var expiredTokens = _usersRefreshTokens.Where(x => x.Value.ExpireAt < now).ToList();
35 foreach (var expiredToken in expiredTokens)
36 {
37 _usersRefreshTokens.TryRemove(expiredToken.Key, out _);
38 }
39 }
40
41 // can be more specific to ip, user agent, device name, etc.
42 public void RemoveRefreshTokenByUserName(string userName)
43 {
44 var refreshTokens = _usersRefreshTokens.Where(x => x.Value.UserName == userName).ToList();
45 foreach (var refreshToken in refreshTokens)
46 {
47 _usersRefreshTokens.TryRemove(refreshToken.Key, out _);
48 }
49 }
50
51 public JwtAuthResult GenerateTokens(string username, Claim[] claims, DateTime now)
52 {
53 var shouldAddAudienceClaim = string.IsNullOrWhiteSpace(claims?.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Aud)?.Value);
54 var jwtToken = new JwtSecurityToken(
55 _jwtTokenConfig.Issuer,
56 shouldAddAudienceClaim ? _jwtTokenConfig.Audience : string.Empty,
57 claims,
58 expires: now.AddMinutes(_jwtTokenConfig.AccessTokenExpiration),
59 signingCredentials: new SigningCredentials(new SymmetricSecurityKey(_secret), SecurityAlgorithms.HmacSha256Signature));
60 var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtToken);
61
62 var refreshToken = new RefreshToken
63 {
64 UserName = username,
65 TokenString = GenerateRefreshTokenString(),
66 ExpireAt = now.AddMinutes(_jwtTokenConfig.RefreshTokenExpiration)
67 };
68 _usersRefreshTokens.AddOrUpdate(refreshToken.TokenString, refreshToken, (s, t) => refreshToken);
69
70 return new JwtAuthResult
71 {
72 AccessToken = accessToken,
73 RefreshToken = refreshToken
74 };
75 }
76
77 public JwtAuthResult Refresh(string refreshToken, string accessToken, DateTime now)
78 {
79 var (principal, jwtToken) = DecodeJwtToken(accessToken);
80 if (jwtToken == null || !jwtToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256Signature))
81 {
82 throw new SecurityTokenException("Invalid token");
83 }
84
85 var userName = principal.Identity.Name;
86 if (!_usersRefreshTokens.TryGetValue(refreshToken, out var existingRefreshToken))
87 {
88 throw new SecurityTokenException("Invalid token");
89 }
90 if (existingRefreshToken.UserName != userName || existingRefreshToken.ExpireAt < now)
91 {
92 throw new SecurityTokenException("Invalid token");
93 }
94
95 return GenerateTokens(userName, principal.Claims.ToArray(), now); // need to recover the original claims
96 }
97
98 public (ClaimsPrincipal, JwtSecurityToken) DecodeJwtToken(string token)
99 {
100 if (string.IsNullOrWhiteSpace(token))
101 {
102 throw new SecurityTokenException("Invalid token");
103 }
104 var principal = new JwtSecurityTokenHandler()
105 .ValidateToken(token,
106 new TokenValidationParameters
107 {
108 ValidateIssuer = true,
109 ValidIssuer = _jwtTokenConfig.Issuer,
110 ValidateIssuerSigningKey = true,
111 IssuerSigningKey = new SymmetricSecurityKey(_secret),
112 ValidAudience = _jwtTokenConfig.Audience,
113 ValidateAudience = true,
114 ValidateLifetime = true,
115 ClockSkew = TimeSpan.FromMinutes(1)
116 },
117 out var validatedToken);
118 return (principal, validatedToken as JwtSecurityToken);
119 }
120
121 private static string GenerateRefreshTokenString()
122 {
123 var randomNumber = new byte[32];
124 using var randomNumberGenerator = RandomNumberGenerator.Create();
125 randomNumberGenerator.GetBytes(randomNumber);
126 return Convert.ToBase64String(randomNumber);
127 }
128 }
129
130 public class JwtAuthResult
131 {
132 [JsonPropertyName("accessToken")]
133 public string AccessToken { get; set; }
134
135 [JsonPropertyName("refreshToken")]
136 public RefreshToken RefreshToken { get; set; }
137 }
138
139 public class RefreshToken
140 {
141 [JsonPropertyName("username")]
142 public string UserName { get; set; } // can be used for usage tracking
143 // can optionally include other metadata, such as user agent, ip address, device name, and so on
144
145 [JsonPropertyName("tokenString")]
146 public string TokenString { get; set; }
147
148 [JsonPropertyName("expireAt")]
149 public DateTime ExpireAt { get; set; }
150 }
151}
Note: See TracBrowser for help on using the repository browser.