-
Notifications
You must be signed in to change notification settings - Fork 0
/
Auth.cs
247 lines (208 loc) · 10.8 KB
/
Auth.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
namespace ExampleWebApp.Backend.WebApi;
public static partial class Extensions
{
/// <summary>
/// Retrieve username and email from given claims principal.
/// </summary>
public static UserClaimsNfo GetUserInfoFromClaims(this ClaimsPrincipal claimsPrincipal)
{
string? userName = claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier);
string? email = claimsPrincipal.FindFirstValue(ClaimTypes.Email);
return new UserClaimsNfo
{
UserName = userName,
Email = email
};
}
/// <summary>
/// Retrieve list of user claims from the given application user.
/// </summary>
public static List<Claim> GetJWTClaims(this UserManager<ApplicationUser> userManager, ApplicationUser user)
{
if (user.UserName is null || user.Email is null)
throw new ArgumentException("username or email null");
var userRoles = userManager.GetRolesAsync(user).GetAwaiter().GetResult();
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.UserName),
new(JwtRegisteredClaimNames.Email, user.Email)
};
foreach (var userRole in userRoles)
{
claims.Add(new Claim(ClaimTypes.Role, userRole));
}
return claims;
}
/// <summary>
/// Adds missing roles to database from <see cref="ROLES_ALL"/> array source.
/// </summary>
public static async Task UpgradeRolesAsync(this WebApplication webApplication)
{
using var scope = webApplication.Services.CreateScope();
var rolemgr = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var currentRoles = rolemgr.Roles.Where(w => w.Name != null).Select(w => w.Name!).ToList();
var rolesToAdd = ROLES_ALL.Where(role => !currentRoles.Contains(role)).ToList();
if (rolesToAdd.Count > 0)
{
// webApplication.Logger.LogInformation($"Adding [{string.Join(',', rolesToAdd)}] roles");
foreach (var roleToAdd in rolesToAdd)
{
await rolemgr.CreateAsync(new IdentityRole(roleToAdd));
}
}
}
/// <summary>
/// Customize cookie name and Secure, HttpOnly, SameSite strict options.
/// </summary>
public static void SetupApplicationCookie(this WebApplicationBuilder builder)
{
builder.Services.ConfigureApplicationCookie(configure =>
{
configure.Cookie.Name = WEB_ApplicationCookieName;
builder.Environment.SetCookieOptions(configure.Cookie);
});
}
/// <summary>
/// Add Identity provider with custom <see cref="ApplicationUser"/> user and system <see cref="IdentityRole"/> role management.
/// Add <see cref="ApplicationDbContext"/> ef store for the identities.
/// Add default token providers.
/// </summary>
public static void SetupIdentityProvider(this IServiceCollection serviceCollection) => serviceCollection
.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.User.RequireUniqueEmail = true;
options.Password.RequiredLength = PASSWORD_MIN_LENGTH;
options.Lockout.DefaultLockoutTimeSpan = LOGIN_FAIL_LOCKOUT_DURATION;
options.Lockout.MaxFailedAccessAttempts = MAX_LOGIN_FAIL_ATTEMPTS;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
/// <summary>
/// Retrieve user roles from given list of claims.
/// </summary>
public static List<string> GetRoles(this IEnumerable<Claim> claims) =>
claims.Where(r => r.Type == ClaimTypes.Role).Select(w => w.Value).ToList();
/// <summary>
/// Get JWT token validation parameters from given options and current configuration.
/// <seealso cref="CONFIG_KEY_JwtSettings_Issuer"/>
/// <seealso cref="CONFIG_KEY_JwtSettings_Audience"/>
/// <seealso cref="CONFIG_KEY_JwtSettings_Key"/>
/// <seealso cref="CONFIG_KEY_JwtSettings_ClockSkewSeconds"/>
/// </summary>
/// <param name="validateIssuer">Will validate issuer (default: true).</param>
/// <param name="validateAudience">Will validate audience (default: true).</param>
/// <param name="validateLifetime">Will validate access token lifetime (default: true).</param>
/// <returns></returns>
public static TokenValidationParameters GetTokenVaildationParameters(this IConfiguration configuration,
bool validateIssuer = true,
bool validateAudience = true,
bool validateLifetime = true) =>
new TokenValidationParameters
{
ValidateIssuer = validateIssuer,
ValidateAudience = validateAudience,
ValidateLifetime = validateLifetime,
ValidateIssuerSigningKey = true,
ValidIssuer = configuration.GetConfigVar(CONFIG_KEY_JwtSettings_Issuer),
ValidAudience = configuration.GetConfigVar(CONFIG_KEY_JwtSettings_Audience),
IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(configuration.GetConfigVar(CONFIG_KEY_JwtSettings_Key))),
ClockSkew = TimeSpan.FromSeconds(configuration.GetConfigVar<int>(CONFIG_KEY_JwtSettings_ClockSkewSeconds)),
};
static object lckRefreshToken = new object();
/// <summary>
/// Add authentication with JWT scheme.
/// Add JWT bearer authentication with handling of failed authentication to resume within refresh token;
/// it also extract jwt from X-Access-Token cookie.
/// </summary>
public static void SetupJWTAuthentication(this WebApplicationBuilder builder) => builder.Services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = builder.Configuration.GetTokenVaildationParameters();
if (options.Events == null) options.Events = new JwtBearerEvents();
options.Events.OnAuthenticationFailed = async context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException) &&
context.HttpContext is not null)
{
// access token expired
var jwtService = context.HttpContext.RequestServices.GetService<IJWTService>();
var userManager = context.HttpContext.RequestServices.GetService<UserManager<ApplicationUser>>();
var hostEnvironment = context.HttpContext.RequestServices.GetService<IHostEnvironment>();
var logger = context.HttpContext.RequestServices.GetService<ILogger<AuthController>>();
var accessToken = context.HttpContext.Request.Cookies[WEB_CookieName_XAccessToken];
var refreshToken = context.HttpContext.Request.Cookies[WEB_CookieName_XRefreshToken];
if (jwtService is not null && hostEnvironment is not null &&
userManager is not null && logger is not null &&
!string.IsNullOrEmpty(accessToken) && !string.IsNullOrEmpty(refreshToken))
{
var principal = jwtService.GetPrincipalFromExpiredToken(accessToken);
var username = principal?.GetUserInfoFromClaims().UserName;
if (username is not null &&
jwtService.IsRefreshTokenStillValid(username, refreshToken))
{
var user = await userManager.FindByNameAsync(username);
var dtNow = DateTime.UtcNow;
var userIsLockedOut = user is not null && await userManager.IsLockedOutAsync(user);
var userDisabled = user is not null && user.Disabled == true;
if (user is not null && !userIsLockedOut && !userDisabled)
{
lock (lckRefreshToken)
{
var res = jwtService.RenewAccessToken(accessToken, refreshToken);
if (res is not null)
{
var opts = new CookieOptions();
hostEnvironment.SetCookieOptions(builder.Configuration, opts, setExpiresAsRefreshToken: true);
context.HttpContext.Response.Cookies.Append(WEB_CookieName_XAccessToken, res.AccessToken, opts);
context.HttpContext.Response.Cookies.Append(WEB_CookieName_XRefreshToken, res.RefreshToken, opts);
context.Principal = res.Principal;
context.Success();
}
}
}
}
}
}
};
options.Events.OnMessageReceived = context =>
{
if (context.Request.Cookies.ContainsKey(WEB_CookieName_XAccessToken))
context.Token = context.Request.Cookies[WEB_CookieName_XAccessToken];
return Task.CompletedTask;
};
});
/// <summary>
/// Configure given CookieBuilder to set Secure, HttpOnly and Strict SameSite options on created cookies.
/// </summary>
public static void SetCookieOptions(this IHostEnvironment environment, CookieBuilder cookieBuilder)
{
cookieBuilder.SecurePolicy = CookieSecurePolicy.Always;
cookieBuilder.HttpOnly = true;
cookieBuilder.SameSite = SameSiteMode.Strict;
}
/// <summary>
/// Configure given CookieOptions to set Secure, HttpOnly and Strict SameSite options on created cookies.
/// </summary>
/// <param name="environment"></param>
/// <param name="configuration"></param>
/// <param name="cookieOptions"></param>
/// <param name="setExpiresAsRefreshToken">if true set expiration time as from JwtSettings:RefreshTokenDurationSeconds</param>
public static void SetCookieOptions(this IHostEnvironment environment, IConfiguration configuration,
CookieOptions cookieOptions, bool setExpiresAsRefreshToken = false)
{
cookieOptions.Secure = true;
cookieOptions.HttpOnly = true;
cookieOptions.SameSite = SameSiteMode.Strict;
if (setExpiresAsRefreshToken)
{
var cookieDuration = TimeSpan.FromSeconds(
configuration.GetConfigVar<int>(CONFIG_KEY_JwtSettings_RefreshTokenDurationSeconds));
cookieOptions.Expires = DateTimeOffset.Now.Add(cookieDuration);
}
}
}