Compare commits

...

18 Commits

Author SHA1 Message Date
Developer 02
d6315ce8a5 chore: Initiliazed Version als 1.0.0.
- Funktionalität zum Ausführen von Swagger in der Produktion hinzugefügt.
2025-01-17 13:49:40 +01:00
Developer 02
7bab2657d4 feat(AuthController): Login-Methode mit Body für Verbraucher-APIs hinzugefügt. 2025-01-15 13:55:47 +01:00
Developer 02
79eaa06ed4 feat(AuthController): Login-Methode für Verbraucher-APIs hinzugefügt. 2025-01-15 13:48:58 +01:00
Developer 02
69efe28310 chore: Added directory search service 2025-01-15 13:17:56 +01:00
Developer 02
cd4428b8f0 reparieren: App-Einstellungen so angeordnet, dass sie Consuemr-Apis erreichen können 2025-01-15 12:58:42 +01:00
Developer 02
a66570bebb feat(AuthController): Erstellt, um Token für Benutzer von UserManager bereitzustellen. 2025-01-15 12:53:51 +01:00
Developer 02
0a3e1566eb Reapply "chore: issuerSigningKeyInitiator konfiguriert, um SecuritKey zu erhalten."
This reverts commit de296d34f3.
2025-01-15 11:02:56 +01:00
Developer 02
de296d34f3 Revert "chore: issuerSigningKeyInitiator konfiguriert, um SecuritKey zu erhalten."
This reverts commit 314bb08125.
2025-01-15 11:01:27 +01:00
Developer 02
314bb08125 chore: issuerSigningKeyInitiator konfiguriert, um SecuritKey zu erhalten. 2025-01-15 11:00:48 +01:00
Developer 02
9c5c0dc61b chore: Eintragungspunktträger in Swagger hinzugefügt 2025-01-15 10:56:33 +01:00
Developer 02
3231aa3299 feat(JwtSignatureHandler): Für Benutzer von in UserManager hinzugefügt. 2025-01-15 10:54:40 +01:00
Developer 02
9d35881327 feat(JwtSignatureHandler): Hinzugefügt für ConsumerApi. 2025-01-15 10:33:16 +01:00
Developer 02
f898d8c4a4 feat(CryptoFactory): Hinzugefügt und in appsettings.json konfiguriert. 2025-01-15 10:30:48 +01:00
Developer 02
82f23d447b chore: Authentifizierung mit layz loading hinzugefügt. 2025-01-15 10:25:51 +01:00
Developer 02
b1bfc46a60 feat(DIExtensions): Zur Verwaltung von Abhängigkeitsinjektionen von ConsumerAPiService. 2025-01-15 10:19:25 +01:00
Developer 02
8e5180188e refactor(ConsumerApiService): Schnittstelle zum Lesen von ConsumerApi und zum Überprüfen der Passwörter erstellt.
- Implementiert als ConsumerApiJsonBasedService, um Daten aus einer json-Datei zu lesen.
2025-01-15 10:04:26 +01:00
Developer 02
404ab74ce1 refactor(consumer.json): umbenannt in consumer-api.json 2025-01-15 09:45:47 +01:00
Developer 02
34b939f75a feat(AuthApiParams): Konfiguriert über IOptions. 2025-01-15 09:42:40 +01:00
8 changed files with 416 additions and 11 deletions

View File

@@ -0,0 +1,183 @@
using DigitalData.Auth.API.Config;
using DigitalData.Core.Abstractions.Security;
using DigitalData.UserManager.Domain.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using DigitalData.UserManager.Application.DTOs.Auth;
using DigitalData.UserManager.Application.Contracts;
using DigitalData.UserManager.Application.DTOs.User;
using DigitalData.Core.Abstractions.Application;
using System.Net;
using DigitalData.Auth.API.Dto;
using DigitalData.Auth.API.Services.Contracts;
namespace DigitalData.Auth.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly IJwtSignatureHandler<UserReadDto> _userSignatureHandler;
private readonly IJwtSignatureHandler<ConsumerApi> _apiSignatureHandler;
private readonly AuthApiParams _apiParams;
private readonly ICryptoFactory _cryptoFactory;
private readonly ILogger<AuthController> _logger;
private readonly IUserService _userService;
private readonly IDirectorySearchService _dirSearchService;
private readonly IConsumerApiService _consumerApiService;
public AuthController(IJwtSignatureHandler<UserReadDto> userSignatureHandler, IOptions<AuthApiParams> cookieParamsOptions, ICryptoFactory cryptoFactory, ILogger<AuthController> logger, IUserService userService, IDirectorySearchService dirSearchService, IConsumerApiService consumerApiService, IJwtSignatureHandler<ConsumerApi> apiSignatureHandler)
{
_apiParams = cookieParamsOptions.Value;
_userSignatureHandler = userSignatureHandler;
_cryptoFactory = cryptoFactory;
_logger = logger;
_userService = userService;
_dirSearchService = dirSearchService;
_consumerApiService = consumerApiService;
_apiSignatureHandler = apiSignatureHandler;
}
private async Task<IActionResult> CreateTokenAsync(LogInDto login, string consumerRoute, bool cookie = true)
{
bool isValid = _dirSearchService.ValidateCredentials(login.Username, login.Password);
if (!isValid)
return Unauthorized();
//find the user
var uRes = await _userService.ReadByUsernameAsync(login.Username);
if (!uRes.IsSuccess || uRes.Data is null)
{
return Unauthorized();
}
if (!_apiParams.Consumers.TryGetByRoute(consumerRoute, out var consumer))
return Unauthorized();
if (!_cryptoFactory.TokenDescriptors.TryGet(_apiParams.Issuer, consumer.Audience, out var descriptor) || descriptor is null)
return StatusCode(StatusCodes.Status500InternalServerError);
var token = _userSignatureHandler.WriteToken(uRes.Data, descriptor);
//set cookie
if (cookie)
{
Response.Cookies.Append(_apiParams.CookieName, token, consumer.CookieOptions.Create(lifetime: descriptor.Lifetime));
return Ok();
}
else
return Ok(token);
}
private async Task<IActionResult> CreateTokenAsync(ConsumerApiLogin login, bool cookie = true)
{
var api = await _consumerApiService.ReadByNameAsync(login.Name);
if (api is null || api.Password != login.Password)
return Unauthorized();
if (!_cryptoFactory.TokenDescriptors.TryGet(_apiParams.Issuer, _apiParams.DefaultConsumer.Audience, out var descriptor) || descriptor is null)
return StatusCode(StatusCodes.Status500InternalServerError);
var token = _apiSignatureHandler!.WriteToken(api, descriptor);
//set cookie
if (cookie)
{
Response.Cookies.Append(_apiParams.CookieName, token, _apiParams.DefaultConsumer.CookieOptions.Create(lifetime: descriptor.Lifetime));
return Ok();
}
else
return Ok(token);
}
//TODO: Add role depends on group name
[HttpPost("~/{consumerRoute}/login")]
[AllowAnonymous]
public async Task<IActionResult> Login([FromForm] LogInDto login, [FromRoute] string consumerRoute)
{
try
{
return await CreateTokenAsync(login, consumerRoute, true);
}
catch (Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("~/login")]
[AllowAnonymous]
public async Task<IActionResult> Login([FromForm] ConsumerApiLogin login)
{
try
{
return await CreateTokenAsync(login, true);
}
catch (Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("logout")]
public IActionResult Logout()
{
try
{
Response.Cookies.Delete(_apiParams.CookieName);
return Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("{consumerRoute}")]
public async Task<IActionResult> CreateTokenViaBody([FromBody] LogInDto login, [FromRoute] string consumerRoute, [FromQuery] bool cookie = false)
{
try
{
return await CreateTokenAsync(login, consumerRoute, cookie);
}
catch (Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost()]
public async Task<IActionResult> CreateTokenViaBody([FromBody] ConsumerApiLogin login, [FromQuery] bool cookie = false)
{
try
{
return await CreateTokenAsync(login, cookie);
}
catch (Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("check")]
[Authorize]
public IActionResult Check() => Ok();
}
}

View File

@@ -4,14 +4,20 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0</AssemblyVersion>
<FileVersion>1.0.0</FileVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.1.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" />
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\" />
<PackageReference Include="UserManager.Application" Version="3.1.0" />
<PackageReference Include="UserManager.Domain" Version="3.0.0" />
<PackageReference Include="UserManager.Infrastructure" Version="3.0.0" />
</ItemGroup>
</Project>

View File

@@ -1,20 +1,132 @@
using DigitalData.Auth.API.Config;
using DigitalData.Auth.API.Dto;
using DigitalData.Auth.API.Services;
using DigitalData.Core.Abstractions.Security;
using DigitalData.Core.Application;
using DigitalData.Core.Security;
using DigitalData.UserManager.Application;
using DigitalData.UserManager.Application.DTOs.User;
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("consumer-api.json", true, true);
var config = builder.Configuration;
builder.Configuration.AddJsonFile("consumers.json", true, true);
var apiParams = config.Get<AuthApiParams>() ?? throw new InvalidOperationException("AuthApiOptions is missing or invalid in appsettings.");
// Add services to the container.
builder.Services.Configure<AuthApiParams>(config);
builder.Services.AddConsumerApiServiceFromConfiguration(config);
builder.Services.AddCryptoFactory(config.GetSection("CryptParams"));
builder.Services.AddJwtSignatureHandler<ConsumerApi>(api => new Dictionary<string, object>
{
{ JwtRegisteredClaimNames.Sub, api.Name },
{ JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds() }
});
builder.Services.AddJwtSignatureHandler<UserReadDto>(user => new Dictionary<string, object>
{
{ JwtRegisteredClaimNames.Sub, user.Id },
{ JwtRegisteredClaimNames.UniqueName, user.Id },
{ JwtRegisteredClaimNames.Email, user.Email ?? string.Empty },
{ JwtRegisteredClaimNames.GivenName, user.Prename ?? string.Empty },
{ JwtRegisteredClaimNames.FamilyName, user.Name ?? string.Empty },
{ JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds() }
});
builder.Services.AddLocalization();
builder.Services.Configure<DirectorySearchOptions>(config.GetSection("DirectorySearchOptions"));
builder.Services.AddDirectorySearchService();
var cnn_str = builder.Configuration.GetConnectionString("Default") ?? throw new InvalidOperationException("Default connection string is not found.");
builder.Services.AddUserManager(cnn_str);
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerGen(options =>
{
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "Enter 'Bearer' [space] and then your valid token."
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header
},
new List<string>()
}
});
});
// Add authentication
Lazy<SecurityKey>? issuerSigningKeyInitiator = null;
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = apiParams!.RequireHttpsMetadata;
options.ClaimsIssuer = apiParams!.Issuer;
options.Audience = apiParams!.DefaultConsumer.Audience;
options.TokenValidationParameters = new()
{
ValidateIssuer = true,
ValidIssuer = apiParams!.Issuer,
ValidateAudience = true,
ValidAudience = apiParams!.DefaultConsumer.Audience,
ValidateLifetime = true,
IssuerSigningKey = issuerSigningKeyInitiator?.Value,
NameClaimType = JwtRegisteredClaimNames.Name,
RoleClaimType = ClaimTypes.Role
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// if there is no token read related cookie
if (context.Token is null // if there is no token
&& context.Request.Cookies.TryGetValue(apiParams!.CookieName, out var token) // get token from cookies
&& token is not null)
context.Token = token;
return Task.CompletedTask;
}
};
});
var app = builder.Build();
issuerSigningKeyInitiator = new Lazy<SecurityKey>(() =>
{
var factory = app.Services.GetRequiredService<ICryptoFactory>();
var desc = factory.TokenDescriptors.Get(apiParams.Issuer, apiParams.DefaultConsumer.Audience);
return desc.Validator.SecurityKey;
});
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
var use_swagger = config.GetValue<bool>("UseSwagger");
if (app.Environment.IsDevelopment() || use_swagger)
{
app.UseSwagger();
app.UseSwaggerUI();
@@ -22,6 +134,8 @@ if (app.Environment.IsDevelopment())
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

View File

@@ -0,0 +1,19 @@
using DigitalData.Auth.API.Dto;
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;
}
}

View File

@@ -0,0 +1,11 @@
using DigitalData.Auth.API.Dto;
namespace DigitalData.Auth.API.Services.Contracts
{
public interface IConsumerApiService
{
public Task<ConsumerApi?> ReadByNameAsync(string name);
public Task<bool> VerifyAsync(string name, string password);
}
}

View File

@@ -0,0 +1,17 @@
using DigitalData.Auth.API.Dto;
using DigitalData.Auth.API.Services.Contracts;
using Microsoft.Extensions.Options;
namespace DigitalData.Auth.API.Services
{
public static class DIExtensions
{
public static IServiceCollection AddConsumerApiServiceFromConfiguration(this IServiceCollection services, IConfiguration configuration, string key = "ConsumerAPIs")
{
var consumerApis = configuration.GetSection("ConsumerAPIs").Get<IEnumerable<ConsumerApi>>() ?? throw new InvalidOperationException($"No Consumer list found in {key} in configuration.");
services.AddSingleton(Options.Create(consumerApis));
services.AddSingleton<IConsumerApiService, ConfiguredConsumerApiService>();
return services;
}
}
}

View File

@@ -5,5 +5,60 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
"UseSwagger": true,
"AllowedHosts": "*",
"ConnectionStrings": {
"Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
},
"DirectorySearchOptions": {
"ServerName": "DD-VMP01-DC01",
"Root": "DC=dd-gan,DC=local,DC=digitaldata,DC=works"
},
"Consumers": [
{
"Route": "api",
"Audience": "api.digitaldata.works"
},
{
"Route": "work-flow",
"Audience": "work-flow.digitaldata.works"
}
],
"Issuer": "auth.digitaldata.works",
"CryptParams": {
"KeySizeInBits": 4096,
"Padding": "OaepSHA512",
"PemDirectory": "Secrets",
"Decryptors": [
{
"IsEncrypted": true
}
],
"TokenDescriptors": [
{
"Id": "4062504f-f081-43d1-b4ed-78256a0879e1",
"Issuer": "auth.digitaldata.works",
"Audience": "api.digitaldata.works",
"IsEncrypted": true,
"Lifetime": "5:00:00"
},
{
"Id": "61c07d26-baa8-4cbb-bb33-ac4ee1838c3a",
"Issuer": "auth.digitaldata.works",
"Audience": "work-flow.digitaldata.works",
"IsEncrypted": true,
"Lifetime": "02:00:00"
}
]
},
"ConsumerAPIs": [
{
"Name": "WorkFlow.API",
"Password": "t3B|aiJ'i-snLzNRj3B{9=&:lM5P@'iL"
},
{
"Name": "DigitalData.UserManager.API",
"Password": "a098Hvu1-y29ep{KPQO]#>8TK+fk{O`_d"
}
]
}

View File

@@ -1,5 +1,5 @@
{
"Consumers": [
"ConsumerAPIs": [
{
"Name": "WorkFlow.API",
"Password": "t3B|aiJ'i-snLzNRj3B{9=&:lM5P@'i<>L"