-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSecurityFeature.cs
More file actions
249 lines (216 loc) · 8.73 KB
/
SecurityFeature.cs
File metadata and controls
249 lines (216 loc) · 8.73 KB
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
248
249
using System.IdentityModel.Tokens.Jwt;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using NetDevPack.Security.JwtExtensions;
using VirtualFinland.UserAPI.Data.Repositories;
using VirtualFinland.UserAPI.Exceptions;
using VirtualFinland.UserAPI.Helpers;
using VirtualFinland.UserAPI.Security.Models;
using JwksExtension = VirtualFinland.UserAPI.Helpers.Extensions.JwksExtension;
namespace VirtualFinland.UserAPI.Security.Features;
public abstract class SecurityFeature : ISecurityFeature
{
///
/// The issuer of the JWT token
///
public string Issuer => _issuer ?? throw new ArgumentNullException($"Issuer of {GetType().Name} is not set");
protected string? _issuer;
/// <summary>
/// Is the security feature initialized
/// </summary>
public bool IsInitialized { get; set; } = false;
/// <summary>
/// Security feature options
/// </summary>
protected SecurityFeatureOptions Options { get; set; }
/// <summary>
/// Cache repository
/// </summary>
protected SecurityClientProviders SecurityClientProviders { get; set; }
protected ICacheRepository CacheRepository;
/// <summary>
/// The URL to the OpenID configuration
/// </summary>
protected string? _openIDConfigurationURL;
/// <summary>
/// The URL to the JWKS options (retrieved from the OpenID configuration)
/// </summary>
protected string? _jwksOptionsUrl;
/// <summary>
/// How many times to retry retrieving the openid configs
/// </summary>
protected const int _configUrlMaxRetryCount = 5;
/// <summary>
/// How long to wait in milliseconds before trying again to retrieve the openid configs
/// </summary>
protected const int _configUrlRetryWaitTime = 3000;
public SecurityFeature(SecurityFeatureOptions options, SecurityClientProviders securityClientProviders)
{
Options = options;
_issuer = options.Issuer;
_openIDConfigurationURL = options.OpenIdConfigurationUrl;
_jwksOptionsUrl = options.AuthorizationJwksJsonUrl;
if (string.IsNullOrEmpty(_openIDConfigurationURL) && string.IsNullOrEmpty(_jwksOptionsUrl))
{
throw new ArgumentException("Invalid security feature configuration");
}
SecurityClientProviders = securityClientProviders;
CacheRepository = SecurityClientProviders.CacheRepositoryFactory.Create(GetType().Name);
}
/// <summary>
/// Builds the authentication configurations
/// </summary>
public void BuildAuthentication(AuthenticationBuilder authentication)
{
LoadOpenIdConfigUrl();
ConfigureOpenIdConnect(authentication);
}
/// <summary>
/// Builds the authorization configurations
/// </summary>
public void BuildAuthorization(AuthorizationOptions options)
{
options.AddPolicy(GetSecurityPolicySchemeName(), policy =>
{
policy.AuthenticationSchemes.Add(GetSecurityPolicySchemeName());
policy.RequireAuthenticatedUser();
});
}
// <summary>
/// Returns the name of the security policy scheme (eg. SinunaSecurityFeatureScheme)
/// </summary>
public string GetSecurityPolicySchemeName()
{
return $"{GetType().Name}Scheme";
}
/// <summary>
/// Resolves the user id from the JWT token
/// </summary>
public virtual string? ResolveTokenUserId(JwtSecurityToken jwtSecurityToken)
{
return jwtSecurityToken.Subject; // the "sub" claim
}
/// <summary>
/// Validates the token audience
/// </summary>
/// <param name="audience"></param>
/// <exception cref="NotAuthorizedException"></exception>
public virtual async Task ValidateSecurityTokenAudience(string audience)
{
if (Options.AudienceGuard.StaticConfig.IsEnabled)
{
await ValidateSecurityTokenAudienceByStaticConfiguration(audience);
}
if (Options.AudienceGuard.Service.IsEnabled)
{
await ValidateSecurityTokenAudienceByService(audience);
}
}
/// <summary>
/// Validates the token audience by static configuration
/// </summary>
/// <param name="audience"></param>
/// <exception cref="NotAuthorizedException"></exception>
public virtual Task ValidateSecurityTokenAudienceByStaticConfiguration(string audience)
{
if (!Options.AudienceGuard.StaticConfig.AllowedAudiences.Contains(audience)) throw new NotAuthorizedException("The given token audience is not allowed");
return Task.CompletedTask;
}
/// <summary>
/// Validates the token audience by external service
/// Implement this in the derived class if using the audience guard service feature
/// </summary>
/// <param name="audience"></param>
/// <exception cref="NotAuthorizedException"></exception>
public virtual Task ValidateSecurityTokenAudienceByService(string audience)
{
throw new NotImplementedException();
}
/// <summary>
/// Configures the OpenID Connect authentication
/// </summary>
protected virtual void ConfigureOpenIdConnect(AuthenticationBuilder authentication)
{
authentication.AddJwtBearer(GetSecurityPolicySchemeName(), c =>
{
JwksExtension.SetJwksOptions(c, new JwkOptions(_jwksOptionsUrl));
c.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateActor = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Issuer
};
});
}
/// <summary>
/// Loads the OpenID configuration URL
/// </summary>
protected virtual async void LoadOpenIdConfigUrl()
{
// Skip if OpenID URL is defined as not required
if (_openIDConfigurationURL == null)
{
IsInitialized = true;
return;
}
if (Options.IsOidcMetadataCachingEnabled && await CacheRepository.Exists(Constants.Cache.OpenIdConfigPrefix))
{
var cachedResult = await CacheRepository.Get<OpenIdConfiguration>(Constants.Cache.OpenIdConfigPrefix);
_issuer = cachedResult.Issuer;
_jwksOptionsUrl = cachedResult.JwksUri;
IsInitialized = true;
return;
}
var httpClient = SecurityClientProviders.HttpClient;
var httpResponse = await httpClient.GetAsync(_openIDConfigurationURL);
for (int retryCount = 0; retryCount < _configUrlMaxRetryCount; retryCount++)
{
if (httpResponse.IsSuccessStatusCode)
{
try
{
var rawData = await httpResponse.Content.ReadAsStringAsync();
var jsonData = JsonNode.Parse(rawData);
_issuer = jsonData?["issuer"]?.ToString();
_jwksOptionsUrl = jsonData?["jwks_uri"]?.ToString();
if (!string.IsNullOrEmpty(_issuer) && !string.IsNullOrEmpty(_jwksOptionsUrl))
{
if (Options.IsOidcMetadataCachingEnabled)
{
// Check for standard cache headers or use default cache duration
var cacheControlHeader = httpResponse.Headers.CacheControl;
var cacheDuration = cacheControlHeader?.MaxAge ?? TimeSpan.FromSeconds(Options.DefaultOidcMetadataCacheDurationInSeconds);
await CacheRepository.Set(Constants.Cache.OpenIdConfigPrefix, new OpenIdConfiguration()
{
Issuer = _issuer,
JwksUri = _jwksOptionsUrl
}, cacheDuration);
IsInitialized = true;
}
break;
}
}
catch (Exception)
{
// Pass
}
}
await Task.Delay(_configUrlRetryWaitTime);
}
// If all retries fail, then send an exception since the security information is critical to the functionality of the backend
if (string.IsNullOrEmpty(_issuer) || string.IsNullOrEmpty(_jwksOptionsUrl))
{
throw new ArgumentNullException("Failed to retrieve OpenID configurations of ${GetType().Name} from ${_openIDConfigurationURL} after ${_configUrlMaxRetryCount} retries.");
}
}
private class OpenIdConfiguration
{
public string Issuer { get; set; } = default!;
public string JwksUri { get; set; } = default!;
}
}