Compare commits
5 Commits
c4f1a9498b
...
0a61586e39
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a61586e39 | ||
|
|
47aeb49a40 | ||
|
|
a1f996b328 | ||
|
|
110b102926 | ||
|
|
ddc55e0fd9 |
@@ -28,5 +28,20 @@
|
||||
public required string Issuer { get; init; }
|
||||
|
||||
public bool RequireHttpsMetadata { get; init; } = true;
|
||||
|
||||
public class Consumer
|
||||
{
|
||||
public required string Route { get; init; }
|
||||
|
||||
public required string Audience { get; init; }
|
||||
|
||||
private CookieOptionsProvider? _cookieOptions;
|
||||
|
||||
#pragma warning disable CS8603 // Possible null reference return.
|
||||
public CookieOptionsProvider CookieOptions { get => _cookieOptions ?? Parent?.DefaultCookieOptions; init => _cookieOptions = value; }
|
||||
#pragma warning restore CS8603 // Possible null reference return.
|
||||
|
||||
internal AuthApiParams? Parent { private get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace DigitalData.Auth.API.Config
|
||||
using static DigitalData.Auth.API.Config.AuthApiParams;
|
||||
|
||||
namespace DigitalData.Auth.API.Config
|
||||
{
|
||||
public static class ConfigExtensions
|
||||
{
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace DigitalData.Auth.API.Config
|
||||
{
|
||||
public class Consumer
|
||||
{
|
||||
public required string Route { get; init; }
|
||||
|
||||
public required string Audience { get; init; }
|
||||
|
||||
private CookieOptionsProvider? _cookieOptions;
|
||||
|
||||
public CookieOptionsProvider CookieOptions { get => _cookieOptions ?? Parent?.DefaultCookieOptions; init => _cookieOptions = value; }
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public AuthApiParams Parent { private get; set; }
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
}
|
||||
}
|
||||
39
src/DigitalData.Auth.API/Config/DependentExtensions.cs
Normal file
39
src/DigitalData.Auth.API/Config/DependentExtensions.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DigitalData.Auth.API.Config
|
||||
{
|
||||
public static class DependentExtensions
|
||||
{
|
||||
private static AuthApiParams? _authApiParams;
|
||||
|
||||
private static AuthApiParams AuthApiParams
|
||||
{
|
||||
get => _authApiParams
|
||||
?? throw new InvalidOperationException(
|
||||
$"DependentExtensions have not been added to the application or are not configured correctly. {typeof(AuthApiParams)} cannot be provided."
|
||||
);
|
||||
set => _authApiParams = value;
|
||||
}
|
||||
|
||||
public static IApplicationBuilder AddDependentExtensions(this IApplicationBuilder application)
|
||||
{
|
||||
var authApiParamOptions = application.ApplicationServices.GetRequiredService<IOptions<AuthApiParams>>();
|
||||
_authApiParams = authApiParamOptions.Value;
|
||||
return application;
|
||||
}
|
||||
|
||||
public static bool TryGetByRoute(this IEnumerable<IAsymmetricTokenDescriptor> descriptors, string consumerRoute, out IAsymmetricTokenDescriptor descriptor)
|
||||
{
|
||||
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
|
||||
descriptor = null;
|
||||
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
|
||||
if (!AuthApiParams.Consumers.TryGetByRoute(consumerRoute, out var consumer)
|
||||
|| !descriptors.TryGet(AuthApiParams.Issuer, consumer.Audience, out var _descriptor))
|
||||
return false;
|
||||
|
||||
descriptor = _descriptor;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace DigitalData.Auth.API.Controllers
|
||||
{
|
||||
private readonly IJwtSignatureHandler<UserReadDto> _userSignatureHandler;
|
||||
|
||||
private readonly IJwtSignatureHandler<ConsumerApi> _apiSignatureHandler;
|
||||
private readonly IJwtSignatureHandler<Consumer> _consumerSignatureHandler;
|
||||
|
||||
private readonly AuthApiParams _apiParams;
|
||||
|
||||
@@ -31,9 +31,9 @@ namespace DigitalData.Auth.API.Controllers
|
||||
|
||||
private readonly IDirectorySearchService _dirSearchService;
|
||||
|
||||
private readonly IConsumerApiService _consumerApiService;
|
||||
private readonly IConsumerService _consumerService;
|
||||
|
||||
public AuthController(IJwtSignatureHandler<UserReadDto> userSignatureHandler, IOptions<AuthApiParams> cookieParamsOptions, ICryptoFactory cryptoFactory, ILogger<AuthController> logger, IUserService userService, IDirectorySearchService dirSearchService, IConsumerApiService consumerApiService, IJwtSignatureHandler<ConsumerApi> apiSignatureHandler)
|
||||
public AuthController(IJwtSignatureHandler<UserReadDto> userSignatureHandler, IOptions<AuthApiParams> cookieParamsOptions, ICryptoFactory cryptoFactory, ILogger<AuthController> logger, IUserService userService, IDirectorySearchService dirSearchService, IConsumerService consumerService, IJwtSignatureHandler<Consumer> apiSignatureHandler)
|
||||
{
|
||||
_apiParams = cookieParamsOptions.Value;
|
||||
_userSignatureHandler = userSignatureHandler;
|
||||
@@ -41,8 +41,8 @@ namespace DigitalData.Auth.API.Controllers
|
||||
_logger = logger;
|
||||
_userService = userService;
|
||||
_dirSearchService = dirSearchService;
|
||||
_consumerApiService = consumerApiService;
|
||||
_apiSignatureHandler = apiSignatureHandler;
|
||||
_consumerService = consumerService;
|
||||
_consumerSignatureHandler = apiSignatureHandler;
|
||||
}
|
||||
|
||||
private async Task<IActionResult> CreateTokenAsync(LogInDto login, string consumerRoute, bool cookie = true)
|
||||
@@ -72,9 +72,9 @@ namespace DigitalData.Auth.API.Controllers
|
||||
return Ok(token);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> CreateTokenAsync(ConsumerApiLogin login, bool cookie = true)
|
||||
private async Task<IActionResult> CreateTokenAsync(ConsumerLogin login, bool cookie = true)
|
||||
{
|
||||
var api = await _consumerApiService.ReadByNameAsync(login.Name);
|
||||
var api = await _consumerService.ReadByIdAsync(login.Id);
|
||||
|
||||
if (api is null || api.Password != login.Password)
|
||||
return Unauthorized();
|
||||
@@ -82,7 +82,7 @@ namespace DigitalData.Auth.API.Controllers
|
||||
if (!_cryptoFactory.TokenDescriptors.TryGet(_apiParams.Issuer, _apiParams.DefaultConsumer.Audience, out var descriptor))
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
|
||||
var token = _apiSignatureHandler.WriteToken(api, descriptor);
|
||||
var token = _consumerSignatureHandler.WriteToken(api, descriptor);
|
||||
|
||||
//set cookie
|
||||
if (cookie)
|
||||
@@ -112,7 +112,7 @@ namespace DigitalData.Auth.API.Controllers
|
||||
|
||||
[HttpPost("login")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login([FromForm] ConsumerApiLogin login)
|
||||
public async Task<IActionResult> Login([FromForm] ConsumerLogin login)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -155,7 +155,7 @@ namespace DigitalData.Auth.API.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateTokenViaBody([FromBody] ConsumerApiLogin login, [FromQuery] bool cookie = false)
|
||||
public async Task<IActionResult> CreateTokenViaBody([FromBody] ConsumerLogin login, [FromQuery] bool cookie = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
17
src/DigitalData.Auth.API/Controllers/ClaimExtensions.cs
Normal file
17
src/DigitalData.Auth.API/Controllers/ClaimExtensions.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace DigitalData.Auth.API.Controllers
|
||||
{
|
||||
public static class ClaimExtensions
|
||||
{
|
||||
public static string? GetName(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
|
||||
public static bool TryGetName(this ClaimsPrincipal user, out string name)
|
||||
{
|
||||
#pragma warning disable CS8601 // Possible null reference assignment.
|
||||
name = user.GetName();
|
||||
#pragma warning restore CS8601 // Possible null reference assignment.
|
||||
return name is not null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,12 @@
|
||||
<PackageReference Include="DigitalData.Core.Application" Version="3.2.0" />
|
||||
<PackageReference Include="DigitalData.Core.Security" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.12" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.3.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.3.1" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
<PackageReference Include="UserManager.Application" Version="3.1.2" />
|
||||
<PackageReference Include="UserManager.Domain" Version="3.0.1" />
|
||||
<PackageReference Include="UserManager.Infrastructure" Version="3.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace DigitalData.Auth.API.Dto
|
||||
{
|
||||
public record ConsumerApiLogin(string Name, string Password);
|
||||
}
|
||||
4
src/DigitalData.Auth.API/Dto/ConsumerLogin.cs
Normal file
4
src/DigitalData.Auth.API/Dto/ConsumerLogin.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace DigitalData.Auth.API.Dto
|
||||
{
|
||||
public record ConsumerLogin(string Id, string Password);
|
||||
}
|
||||
4
src/DigitalData.Auth.API/Entities/Consumer.cs
Normal file
4
src/DigitalData.Auth.API/Entities/Consumer.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace DigitalData.Auth.API.Entities
|
||||
{
|
||||
public record Consumer(string Id, string Password, string Audience);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace DigitalData.Auth.API.Entities
|
||||
{
|
||||
public record ConsumerApi(string Name, string Password);
|
||||
}
|
||||
@@ -6,16 +6,14 @@ using DigitalData.Core.Application;
|
||||
using DigitalData.Core.Security;
|
||||
using DigitalData.UserManager.Application;
|
||||
using DigitalData.UserManager.Application.DTOs.User;
|
||||
using DigitalData.UserManager.Application.Services;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using System.Security.Claims;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Configuration.AddJsonFile("consumers-api.json", true, true);
|
||||
builder.Configuration.AddJsonFile("consumer-repository.json", true, true);
|
||||
|
||||
var config = builder.Configuration;
|
||||
|
||||
@@ -23,11 +21,11 @@ var apiParams = config.Get<AuthApiParams>() ?? throw new InvalidOperationExcepti
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.Configure<AuthApiParams>(config);
|
||||
builder.Services.AddConsumerApiServiceFromConfiguration(config);
|
||||
builder.Services.AddConsumerApiService(config);
|
||||
builder.Services.AddCryptoFactory(config.GetSection("CryptParams"));
|
||||
builder.Services.AddJwtSignatureHandler<ConsumerApi>(api => new Dictionary<string, object>
|
||||
builder.Services.AddJwtSignatureHandler<Consumer>(api => new Dictionary<string, object>
|
||||
{
|
||||
{ JwtRegisteredClaimNames.Sub, api.Name },
|
||||
{ JwtRegisteredClaimNames.Sub, api.Id },
|
||||
{ JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds() }
|
||||
});
|
||||
builder.Services.AddJwtSignatureHandler<UserReadDto>(user => new Dictionary<string, object>
|
||||
@@ -95,9 +93,7 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
ValidateAudience = true,
|
||||
ValidAudience = apiParams!.DefaultConsumer.Audience,
|
||||
ValidateLifetime = true,
|
||||
IssuerSigningKey = issuerSigningKeyInitiator?.Value,
|
||||
NameClaimType = JwtRegisteredClaimNames.Name,
|
||||
RoleClaimType = ClaimTypes.Role
|
||||
IssuerSigningKey = issuerSigningKeyInitiator?.Value
|
||||
};
|
||||
|
||||
options.Events = new JwtBearerEvents
|
||||
@@ -116,6 +112,8 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.AddDependentExtensions();
|
||||
|
||||
issuerSigningKeyInitiator = new Lazy<SecurityKey>(() =>
|
||||
{
|
||||
var factory = app.Services.GetRequiredService<ICryptoFactory>();
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using DigitalData.Auth.API.Entities;
|
||||
using DigitalData.Auth.API.Services.Contracts;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DigitalData.Auth.API.Services
|
||||
{
|
||||
public class ConfiguredConsumerApiService : IConsumerApiService
|
||||
{
|
||||
private readonly IEnumerable<ConsumerApi> _consumerAPIs;
|
||||
public ConfiguredConsumerApiService(IOptions<IEnumerable<ConsumerApi>> options)
|
||||
{
|
||||
_consumerAPIs = options.Value;
|
||||
}
|
||||
|
||||
public Task<ConsumerApi?> ReadByNameAsync(string name) => Task.Run(() => _consumerAPIs.FirstOrDefault(api => api.Name == name));
|
||||
|
||||
public async Task<bool> VerifyAsync(string name, string password) => (await ReadByNameAsync(name))?.Password == password;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using DigitalData.Auth.API.Entities;
|
||||
using DigitalData.Auth.API.Services.Contracts;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DigitalData.Auth.API.Services
|
||||
{
|
||||
public class ConfiguredConsumerService : IConsumerService
|
||||
{
|
||||
private readonly IEnumerable<Consumer> _consumerAPIs;
|
||||
public ConfiguredConsumerService(IOptions<IEnumerable<Consumer>> options)
|
||||
{
|
||||
_consumerAPIs = options.Value;
|
||||
}
|
||||
|
||||
public Task<Consumer?> ReadByIdAsync(string id) => Task.Run(() => _consumerAPIs.FirstOrDefault(api => api.Id == id));
|
||||
|
||||
public async Task<bool> VerifyAsync(string id, string password) => (await ReadByIdAsync(id))?.Password == password;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using DigitalData.Auth.API.Entities;
|
||||
|
||||
namespace DigitalData.Auth.API.Services.Contracts
|
||||
{
|
||||
public interface IConsumerApiService
|
||||
{
|
||||
public Task<ConsumerApi?> ReadByNameAsync(string name);
|
||||
|
||||
public Task<bool> VerifyAsync(string name, string password);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using DigitalData.Auth.API.Entities;
|
||||
|
||||
namespace DigitalData.Auth.API.Services.Contracts
|
||||
{
|
||||
public interface IConsumerService
|
||||
{
|
||||
public Task<Consumer?> ReadByIdAsync(string id);
|
||||
|
||||
public Task<bool> VerifyAsync(string id, string password);
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,11 @@ namespace DigitalData.Auth.API.Services
|
||||
{
|
||||
public static class DIExtensions
|
||||
{
|
||||
public static IServiceCollection AddConsumerApiServiceFromConfiguration(this IServiceCollection services, IConfiguration configuration, string key = "ConsumerAPIs")
|
||||
public static IServiceCollection AddConsumerApiService(this IServiceCollection services, IConfiguration configuration, string key = "ConsumerAPIs")
|
||||
{
|
||||
var consumerApis = configuration.GetSection(key).Get<IEnumerable<ConsumerApi>>() ?? throw new InvalidOperationException($"No Consumer list found in {key} in configuration.");
|
||||
services.AddSingleton(Options.Create(consumerApis));
|
||||
services.AddSingleton<IConsumerApiService, ConfiguredConsumerApiService>();
|
||||
var consumers = configuration.GetSection(key).Get<IEnumerable<Consumer>>() ?? throw new InvalidOperationException($"No Consumer list found in {key} in configuration.");
|
||||
services.AddSingleton(Options.Create(consumers));
|
||||
services.AddSingleton<IConsumerService, ConfiguredConsumerService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
19
src/DigitalData.Auth.API/consumer-repository.json
Normal file
19
src/DigitalData.Auth.API/consumer-repository.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"ConsumerAPIs": [
|
||||
{
|
||||
"Id": "auth",
|
||||
"Audience": "auth.digitaldata.works",
|
||||
"Password": "aQ9z!2@TgY7b#fHcD3pLmV1$wX"
|
||||
},
|
||||
{
|
||||
"Id": "work-flow",
|
||||
"Audience": "work-flow.digitaldata.works",
|
||||
"Password": "t3B|aiJ'i-snLzNRj3B{9=&:lM5P@'iL"
|
||||
},
|
||||
{
|
||||
"Id": "user-manager",
|
||||
"Audience": "user-manager.digitaldata.works",
|
||||
"Password": "a098Hvu1-y29ep{KPQO]#>8TK+fk{O`_d"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"ConsumerAPIs": [
|
||||
{
|
||||
"Name": "WorkFlow.API",
|
||||
"Password": "t3B|aiJ'i-snLzNRj3B{9=&:lM5P@'iL"
|
||||
},
|
||||
{
|
||||
"Name": "DigitalData.UserManager.API",
|
||||
"Password": "a098Hvu1-y29ep{KPQO]#>8TK+fk{O`_d"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user