Skip to content

Commit 86aabca

Browse files
authored
Merge pull request #71 from Virtual-Finland-Development/VFD-264-vahvennetaan-tuotannon-palvelujen-kayttooikeudet
Protect the users API endpoint with web ACL
2 parents d9297c3 + 1ccff16 commit 86aabca

21 files changed

Lines changed: 331 additions & 191 deletions

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Activities/Productizer/ProductizerController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace VirtualFinland.UserAPI.Activities.Productizer;
1313

1414
[ApiController]
1515
[Authorize]
16+
[Authorize(Policy = "RequestFromDataspace")]
1617
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
1718
[Produces("application/json")]
1819
public class ProductizerController : ControllerBase

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Activities/User/UserController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace VirtualFinland.UserAPI.Activities.User;
1111

1212
[ApiController]
1313
[Authorize]
14+
[Authorize(Policy = "RequestFromAccessFinland")]
1415
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
1516
[Produces("application/json")]
1617
public class UserController : ApiControllerBase

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Helpers/Constants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public static class Web
1111
public static class Security
1212
{
1313
public static string ResolvePolicyFromTokenIssuer => "ResolvePolicyFromTokenIssuer";
14+
public static string RequestFromAccessFinland => "RequestFromAccessFinland";
15+
public static string RequestFromDataspace => "RequestFromDataspace";
1416
}
1517

1618
public static class Headers

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
using VirtualFinland.UserAPI.Middleware;
1515
using VirtualFinland.UserAPI.Helpers.Extensions;
1616
using VirtualFinland.UserAPI.Security.Extensions;
17+
using VirtualFinland.UserAPI.Security.AccessRequirements;
18+
using VirtualFinland.UserAPI.Security.Configurations;
1719

1820
Log.Logger = new LoggerConfiguration()
1921
.WriteTo.Console()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
namespace VirtualFinland.UserAPI.Security.AccessRequirements;
3+
4+
public sealed class RequestAccessConfig
5+
{
6+
public string HeaderName { get; init; } = default!;
7+
public List<string> AccessKeys { get; init; } = default!;
8+
public bool IsEnabled { get; init; } = default!;
9+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.Extensions.Options;
3+
4+
namespace VirtualFinland.UserAPI.Security.AccessRequirements;
5+
6+
public class RequestAccessRequirement : AuthorizationHandler<RequestAccessRequirement>, IAuthorizationRequirement
7+
{
8+
private readonly RequestAccessConfig _config;
9+
public RequestAccessRequirement(RequestAccessConfig config)
10+
{
11+
_config = config;
12+
}
13+
14+
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequestAccessRequirement requirement)
15+
{
16+
if (!_config.IsEnabled)
17+
context.Succeed(requirement);
18+
19+
if (context.Resource is DefaultHttpContext httpContext &&
20+
httpContext.Request.Headers.TryGetValue(_config.HeaderName, out var apiKeyHeader))
21+
{
22+
string apiKey = apiKeyHeader.ToString();
23+
if (IsValidApiKey(apiKey))
24+
{
25+
context.Succeed(requirement);
26+
}
27+
}
28+
return Task.CompletedTask;
29+
}
30+
31+
private bool IsValidApiKey(string apiKey)
32+
{
33+
foreach (var accessKey in _config.AccessKeys)
34+
{
35+
if (accessKey == "") continue;
36+
if (apiKey == accessKey)
37+
return true;
38+
}
39+
return false;
40+
}
41+
}

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Security/ApplicationSecurity.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,10 @@ public JwtTokenResult ParseJwtToken(string token)
2828

2929
// Resolve the security feature by token issuer (must be enabled) // @TODO: ensure the security feature is loaded before this
3030
var tokenIssuer = parsedToken.Issuer;
31-
var securityFeature = _features.Find(o => o.Issuer == tokenIssuer);
32-
if (securityFeature == null) throw new NotAuthorizedException("The given token issuer is not valid");
31+
var securityFeature = _features.Find(o => o.Issuer == tokenIssuer) ?? throw new NotAuthorizedException("The given token issuer is not valid");
3332

3433
// Resolve user id
35-
var userId = securityFeature.ResolveTokenUserId(parsedToken);
36-
if (userId == null) throw new NotAuthorizedException("The given token claim is not valid");
37-
34+
var userId = securityFeature.ResolveTokenUserId(parsedToken) ?? throw new NotAuthorizedException("The given token claim is not valid");
3835
return new JwtTokenResult { UserId = userId, Issuer = securityFeature.Issuer };
3936
}
4037
}

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Security/Configurations/TestBedConsentProviderConfig.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ public class TestBedConsentProviderConfig : IConsentProviderConfig
2727

2828
public TestBedConsentProviderConfig(IConfiguration configuration)
2929
{
30-
_jwksJsonUrl = configuration["Security:Testbed:ConsentJwksJsonUrl"];
31-
Issuer = configuration["Security:Testbed:ConsentIssuer"];
32-
ConsentVerifyUrl = configuration["Security:Testbed:ConsentVerifyUrl"];
30+
_jwksJsonUrl = configuration["Security:Authorization:Testbed:ConsentJwksJsonUrl"];
31+
Issuer = configuration["Security:Authorization:Testbed:ConsentIssuer"];
32+
ConsentVerifyUrl = configuration["Security:Authorization:Testbed:ConsentVerifyUrl"];
3333
}
3434

3535
public async void LoadPublicKeys()

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Security/Extensions/ConsentServiceProviderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public static IServiceCollection RegisterConsentServiceProviders(this IServiceCo
1515
var testBedConsentProviderConfig = new TestBedConsentProviderConfig(configuration);
1616
services.AddSingleton<IConsentProviderConfig>(testBedConsentProviderConfig);
1717

18-
if (configuration.GetValue<bool>("Security:Testbed:IsEnabled"))
18+
if (configuration.GetValue<bool>("Security:Authorization:Testbed:IsEnabled"))
1919
{
2020
testBedConsentProviderConfig.LoadPublicKeys();
2121
}

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Security/Extensions/SecurityFeatureServiceExtensions.cs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.Net.Http.Headers;
33
using VirtualFinland.UserAPI.Exceptions;
44
using VirtualFinland.UserAPI.Helpers;
5+
using VirtualFinland.UserAPI.Security.AccessRequirements;
56
using VirtualFinland.UserAPI.Security.Features;
67
using VirtualFinland.UserAPI.Security.Models;
78

@@ -19,19 +20,15 @@ public static IServiceCollection RegisterSecurityFeatures(this IServiceCollectio
1920
{
2021
var features = new List<ISecurityFeature>();
2122

22-
var securityConfigurations = configuration.GetSection("Security").Get<Dictionary<string, SecurityFeatureOptions>>();
23+
var securityConfigurations = configuration.GetSection("Security:Authorization").Get<Dictionary<string, SecurityFeatureOptions>>();
2324
var enabledSecurityFeatureNames = securityConfigurations.Where(x => x.Value.IsEnabled).Select(x => x.Key).ToArray();
2425
if (!enabledSecurityFeatureNames.Any()) throw new ArgumentException("No security features enabled");
2526

26-
// Map security feature name to correct class
27+
// Dynamically map security feature name to correct class
2728
foreach (var securityFeatureName in enabledSecurityFeatureNames)
2829
{
29-
var securityFeatureType = Type.GetType($"VirtualFinland.UserAPI.Security.Features.{securityFeatureName}SecurityFeature");
30-
if (securityFeatureType is null) throw new ArgumentException($"Security feature {securityFeatureName} not found");
31-
32-
var securityFeature = Activator.CreateInstance(securityFeatureType, securityConfigurations[securityFeatureName]) as ISecurityFeature;
33-
if (securityFeature is null) throw new ArgumentException($"Security feature {securityFeatureName} not found");
34-
30+
var securityFeatureType = Type.GetType($"VirtualFinland.UserAPI.Security.Features.{securityFeatureName}SecurityFeature") ?? throw new ArgumentException($"Security feature {securityFeatureName} not found");
31+
var securityFeature = Activator.CreateInstance(securityFeatureType, securityConfigurations[securityFeatureName]) as ISecurityFeature ?? throw new ArgumentException($"Security feature {securityFeatureName} not found");
3532
features.Add(securityFeature);
3633
}
3734

@@ -51,6 +48,12 @@ public static IServiceCollection RegisterSecurityFeatures(this IServiceCollectio
5148
services.AddAuthorization(options =>
5249
{
5350
foreach (var securityFeature in features) securityFeature.BuildAuthorization(options);
51+
52+
// Add special policies
53+
options.AddPolicy(Constants.Security.RequestFromAccessFinland, policy =>
54+
policy.Requirements.Add(new RequestAccessRequirement(configuration.GetSection("Security:Access:AccessFinland").Get<RequestAccessConfig>())));
55+
options.AddPolicy(Constants.Security.RequestFromDataspace, policy =>
56+
policy.Requirements.Add(new RequestAccessRequirement(configuration.GetSection("Security:Access:Dataspace").Get<RequestAccessConfig>())));
5457
});
5558

5659
authenticationBuilder.AddPolicyScheme(Constants.Security.ResolvePolicyFromTokenIssuer, Constants.Security.ResolvePolicyFromTokenIssuer,
@@ -68,7 +71,7 @@ private static string GetSecurityPolicySchemeName(IHeaderDictionary headers, IEn
6871
var authorizationHeader = headers[HeaderNames.Authorization].FirstOrDefault();
6972

7073
if (string.IsNullOrEmpty(authorizationHeader) || !authorizationHeader.StartsWith("Bearer "))
71-
throw new NotAuthorizedException("Invalid token provided");
74+
throw new NotAuthorizedException("No token provided");
7275

7376
var token = authorizationHeader["Bearer ".Length..].Trim();
7477

@@ -78,11 +81,7 @@ private static string GetSecurityPolicySchemeName(IHeaderDictionary headers, IEn
7881

7982
var issuer = jwtHandler.ReadJwtToken(token).Issuer;
8083

81-
var feature = features.SingleOrDefault(securityFeature => securityFeature.Issuer == issuer);
82-
83-
if (feature is null)
84-
throw new NotAuthorizedException("Invalid token provided");
85-
84+
var feature = features.SingleOrDefault(securityFeature => securityFeature.Issuer == issuer) ?? throw new NotAuthorizedException("Invalid token provider");
8685
return feature.GetSecurityPolicySchemeName();
8786
}
8887
}

0 commit comments

Comments
 (0)