Compare commits

...

13 Commits

Author SHA1 Message Date
Developer 02
a325d07c6b refactor: AuthController-Methoden optimieren und Login-Logik verbessern
- AuthController aktualisiert, um eine klarere Struktur zu implementieren.
- Login-Methode vereinfacht, um die Benutzerauthentifizierung direkt zu behandeln.
- Neue LoginById-Methode für den Benutzerlogin über ID eingeführt.
- Fehlerprotokollierung und -behandlung in den Login-Methoden verbessert.
- Überflüssigen Code entfernt und Lesbarkeit verbessert.
- TODO für weitere Integration mit UserManager hinzugefügt.
2024-10-29 14:24:13 +01:00
Developer 02
69abd3afa2 feat(DIExtensions.AddAPIKeyAuth): if options.Key is null return true in default isValidKey function 2024-10-29 12:44:53 +01:00
Developer 02
cbdd6ee295 feat(APIKeyAuthOptions): Schlüsselattribut wird löschbar gemacht.
- isValidKey-Eintrag wird löschbar gemacht.
 - wenn der Schlüssel null ist und der X-API-Schlüssel nicht existiert, wird die Anfrage authirezred.
2024-10-29 12:23:10 +01:00
Developer 02
2c1abaaf32 feat(DisableAPIKeyAuth): Optionen als bool zu appsettings hinzugefügt. Konfiguriert mit app 2024-10-29 11:43:25 +01:00
Developer 02
8038ff74dd feat(Swagger): OpenApiInfo über appsettings konfiguriert. 2024-10-29 10:44:20 +01:00
Developer 02
edcf3781b7 feat(APIKeyAuthAttribute): Zu allen Controllern hinzugefügt 2024-10-29 10:21:10 +01:00
Developer 02
6ea053be36 feat(APIKeyAuthHeaderOpFilter): Implementierung der SwaggerGen.IOperationFilter-Schnittstelle, um das API-Schlüsselfeld hinzuzufügen.
- Eigenschaft swagger-description hinzugefügt
2024-10-29 10:00:06 +01:00
Developer 02
67a62d7311 feat(APIKeyAuthOptions): Datenmodell zur Konfiguration der Autorisierung mit API-Schlüssel erstellt.
- DI-Erweiterung hinzugefügt
2024-10-29 09:29:14 +01:00
Developer 02
e17875dad7 feat(API): Methode zur Injektion von Abhängigkeiten hinzugefügt, um API-Schlüssel-Filter hinzuzufügen 2024-10-28 16:58:50 +01:00
Developer 02
b460de4e37 feat: Attribut zur API-Schlüssel-Authentifizierung hinzufügen
- ApiKeyAuthAttribute hinzugefügt und mit ApiKeyAuthFilter verknüpft.
- Dieses Attribut wird verwendet, um die API-Schlüssel-Überprüfung in Controllern anzuwenden.
2024-10-28 16:48:58 +01:00
Developer 02
1ca336abe0 feat: API-Schlüssel-Authentifizierungsfilter implementieren
- ApiKeyAuthFilter hinzugefügt, um API-Schlüssel aus den Anforderungs-Headern zu validieren.
- Der Filter überprüft das Vorhandensein eines API-Schlüssels und dessen Gültigkeit, bevor die Anforderung autorisiert wird.
- Der Standardname des API-Schlüssel-Headers ist auf "X-API-Key" festgelegt.
2024-10-28 16:45:22 +01:00
Developer 02
6e4a575864 feat(API): NLogger hinzugefügt 2024-10-28 16:27:32 +01:00
Developer 02
d664adf000 refactor: Unnötige Verzeichnissuch-Cachinge-Prozesse entfernen 2024-10-28 16:13:55 +01:00
18 changed files with 329 additions and 116 deletions

View File

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
using WorkFlow.API.Filters;
namespace WorkFlow.API.Attributes
{
//TODO: move APIKeyAuthAttribute to Core.API
public class APIKeyAuthAttribute : ServiceFilterAttribute
{
public APIKeyAuthAttribute()
: base(typeof(APIKeyAuthFilter))
{
}
}
}

View File

@ -10,19 +10,16 @@ using DigitalData.Core.Abstractions.Application;
using Microsoft.Extensions.Localization;
using DigitalData.Core.DTO;
using WorkFlow.API.Models;
using WorkFlow.API.Attributes;
namespace WorkFlow.API.Controllers
{
//TODO: implement up-to-date AuthController in UserManager
[APIKeyAuth]
[Route("api/[controller]")]
[ApiController]
public class AuthController(IUserService userService, IGroupOfUserService gouService, IDirectorySearchService directorySearchService, IStringLocalizer<Resource> localizer, ILogger<AuthController> logger) : ControllerBase
public class AuthController(IUserService userService, IDirectorySearchService dirSearchService, IStringLocalizer<Resource> localizer, ILogger<AuthController> logger) : ControllerBase
{
private readonly IUserService _userService = userService;
private readonly IGroupOfUserService _gouService = gouService;
private readonly IDirectorySearchService _dirSearchService = directorySearchService;
private readonly IStringLocalizer<Resource> _localizer = localizer;
private readonly ILogger<AuthController> _logger = logger;
[AllowAnonymous]
[HttpGet("check")]
public IActionResult CheckAuthentication()
@ -33,78 +30,81 @@ namespace WorkFlow.API.Controllers
}
catch(Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[NonAction]
public async Task<IActionResult> Login(UserReadDto user)
{
// Create claimsIdentity
var claimsIdentity = new ClaimsIdentity(user.ToClaimList(), CookieAuthenticationDefaults.AuthenticationScheme);
// Create authProperties
var authProperties = new AuthenticationProperties
{
IsPersistent = true,
AllowRefresh = true,
ExpiresUtc = DateTime.UtcNow.AddMinutes(60)
};
// Sign in
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
return Ok();
}
[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] Login login)
{
try
{
var username = string.Empty;
DataResult<UserReadDto>? uRes = null;
if(login.Username is not null && login.UserId is not null)
return BadRequest("Invalid request: either 'UserId' or 'Username' must be provided, but not both.");
else if(login.Username is not null)
username = login.Username;
else if(login.UserId is int userId)
{
uRes = await _userService.ReadByIdAsync(userId);
if (!uRes.IsSuccess || uRes.Data is null)
{
return Unauthorized(uRes);
}
}
else
return BadRequest("Invalid request: either 'UserId' or 'Username' must be provided, but not both.");
bool isValid = _dirSearchService.ValidateCredentials(username, login.Password);
if (!isValid)
return Unauthorized(Result.Fail().Message(_localizer[Key.UserNotFound]));
var gouMsg = await _gouService.HasGroup(username, "PM_USER", caseSensitive: false);
if (!gouMsg.IsSuccess)
return Unauthorized(Result.Fail().Message(_localizer[Key.UnauthorizedUser]));
//find the user
uRes ??= await _userService.ReadByUsernameAsync(username);
if (!uRes.IsSuccess || uRes.Data is null)
{
_logger.LogNotice(uRes.Notices);
return Unauthorized();
}
UserReadDto user = uRes.Data;
// Create claimsIdentity
var claimsIdentity = new ClaimsIdentity(user.ToClaimList(), CookieAuthenticationDefaults.AuthenticationScheme);
// Create authProperties
var authProperties = new AuthenticationProperties
{
IsPersistent = true,
AllowRefresh = true,
ExpiresUtc = DateTime.UtcNow.AddMinutes(60)
};
// Sign in
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
_dirSearchService.SetSearchRootCache(user.Username, login.Password);
return Ok();
return dirSearchService.ValidateCredentials(login.Username, login.Password)
? await userService.ReadByUsernameAsync(login.Username).ThenAsync(
SuccessAsync: Login,
Fail: IActionResult (msg, ntc) =>
{
logger.LogNotice(ntc);
logger.LogError("User could not be found, although verified by directory-search-service. It needs to be imported by UserManager. User name is {username}.", login.Username);
return StatusCode(StatusCodes.Status500InternalServerError);
})
: Unauthorized(localizer[WFKey.UserNotFoundOrWrongPassword].Value);
}
catch(Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[AllowAnonymous]
[HttpPost("login/{id}")]
public async Task<IActionResult> LoginById([FromRoute] int id, [FromQuery] string password)
{
try
{
return await userService.ReadByIdAsync(id).ThenAsync(
SuccessAsync: async user
=> dirSearchService.ValidateCredentials(user.Username, password)
? await Login(user)
: Unauthorized(localizer[WFKey.WrongPassword].Value),
Fail: (msg, ntc) =>
{
if (ntc.HasFlag(Flag.NotFound))
return Unauthorized(Key.UserNotFound);
logger.LogNotice(ntc);
return StatusCode(StatusCodes.Status500InternalServerError);
});
}
catch (Exception ex)
{
logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
@ -121,16 +121,16 @@ namespace WorkFlow.API.Controllers
if (string.IsNullOrEmpty(username))
return Unauthorized();
return await _userService.ReadByUsernameAsync(username)
return await userService.ReadByUsernameAsync(username)
.ThenAsync(Ok, IActionResult (m, n) =>
{
_logger.LogNotice(n);
return NotFound(Result.Fail().Message(_localizer[Key.UserNotFound]));
logger.LogNotice(n);
return NotFound(Result.Fail().Message(localizer[Key.UserNotFound].Value));
});
}
catch (Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
@ -146,7 +146,7 @@ namespace WorkFlow.API.Controllers
}
catch(Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}

View File

@ -1,12 +1,14 @@
using DigitalData.Core.API;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WorkFlow.API.Attributes;
using WorkFlow.Application.Contracts;
using WorkFlow.Application.DTO.Config;
using WorkFlow.Domain.Entities;
namespace WorkFlow.API.Controllers
{
[APIKeyAuth]
[Route("api/[controller]")]
[ApiController]
[Authorize]

View File

@ -1,8 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using WorkFlow.API.Attributes;
namespace WorkFlow.API.Controllers
{
[APIKeyAuth]
public static class ControllerExtensions
{
public static bool TryGetUserId(this ControllerBase controller, out int? id)

View File

@ -1,12 +1,14 @@
using DigitalData.Core.API;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WorkFlow.API.Attributes;
using WorkFlow.Application.Contracts;
using WorkFlow.Application.DTO.Profile;
using WorkFlow.Domain.Entities;
namespace WorkFlow.API.Controllers
{
[APIKeyAuth]
[Route("api/[controller]")]
[ApiController]
[Authorize]

View File

@ -2,12 +2,14 @@
using DigitalData.Core.DTO;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WorkFlow.API.Attributes;
using WorkFlow.Application.Contracts;
using WorkFlow.Application.DTO.ProfileControlsTF;
using WorkFlow.Domain.Entities;
namespace WorkFlow.API.Controllers
{
[APIKeyAuth]
[Route("api/[controller]")]
[ApiController]
[Authorize]

View File

@ -2,12 +2,14 @@
using DigitalData.Core.DTO;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WorkFlow.API.Attributes;
using WorkFlow.Application.Contracts;
using WorkFlow.Application.DTO.ProfileObjState;
using WorkFlow.Domain.Entities;
namespace WorkFlow.API.Controllers
{
[APIKeyAuth]
[Route("api/[controller]")]
[ApiController]
[Authorize]

View File

@ -1,12 +1,14 @@
using DigitalData.Core.API;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WorkFlow.API.Attributes;
using WorkFlow.Application.Contracts;
using WorkFlow.Application.DTO.State;
using WorkFlow.Domain.Entities;
namespace WorkFlow.API.Controllers
{
[APIKeyAuth]
[Route("api/[controller]")]
[ApiController]
[Authorize]

View File

@ -2,9 +2,11 @@
using DigitalData.UserManager.Application.Contracts;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WorkFlow.API.Attributes;
namespace WorkFlow.API.Controllers
{
[APIKeyAuth]
[Route("api/[controller]")]
[ApiController]
[Authorize]

View File

@ -0,0 +1,22 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using WorkFlow.API.Filters;
using WorkFlow.API.Models;
namespace WorkFlow.API.Extensions
{
public static class DIExtensions
{
public static IServiceCollection AddAPIKeyAuth(this IServiceCollection services, Func<string?, bool> isValidKey, string headerName = "X-API-Key")
=> services.AddSingleton<APIKeyAuthFilter>(provider => new(isValidKey: isValidKey, headerName: headerName));
public static IServiceCollection AddAPIKeyAuth(this IServiceCollection services, APIKeyAuthOptions options, bool configureOptions = true)
{
if(configureOptions)
services.TryAddSingleton(Options.Create(options));
return services.AddAPIKeyAuth(isValidKey: key => options.Key is null || options.Key == key, headerName: options.HeaderName);
}
}
}

View File

@ -0,0 +1,36 @@
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using WorkFlow.API.Models;
namespace WorkFlow.API.Filters
{
public class APIKeyAuthHeaderOpFilter(IOptions<APIKeyAuthOptions> options, IWebHostEnvironment environment) : IOperationFilter
{
private readonly APIKeyAuthOptions apiKeyAuthOptions = options.Value;
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var param = new OpenApiParameter
{
Name = apiKeyAuthOptions.HeaderName,
In = ParameterLocation.Header,
Required = true,
AllowEmptyValue = false,
Schema = new OpenApiSchema
{
Type = "string"
}
};
if(environment.IsDevelopment())
param.Schema.Default = new OpenApiString(apiKeyAuthOptions.Key);
if (apiKeyAuthOptions.SwaggerDescription is not null)
param.Description = apiKeyAuthOptions.SwaggerDescription;
operation.Parameters.Add(param);
}
}
}

View File

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
namespace WorkFlow.API.Filters
{
public class APIKeyAuthFilter(Func<string?, bool> isValidKey, string headerName = "X-API-Key") : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (!isValidKey(context.HttpContext.Request.Headers[headerName]))
context.Result = new UnauthorizedResult();
}
}
}

View File

@ -0,0 +1,11 @@
namespace WorkFlow.API.Models
{
public class APIKeyAuthOptions
{
public string? Key { get; init; } = null;
public string HeaderName { get; init; } = "X-API-Key";
public string? SwaggerDescription { get; init; } = null;
}
}

View File

@ -1,4 +1,4 @@
namespace WorkFlow.API.Models
{
public record Login(int? UserId, string? Username, string Password);
public record Login(string Username, string Password);
}

View File

@ -8,58 +8,93 @@ using DigitalData.Core.Application;
using DigitalData.UserManager.Application.DTOs.User;
using Microsoft.IdentityModel.Tokens;
using WorkFlow.API.Models;
using System.Security.Claims;
using NLog;
using NLog.Web;
using WorkFlow.API.Extensions;
using WorkFlow.API.Filters;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized.");
// Add services to the container.
var cnn_str = config.GetConnectionString("Default") ?? throw new ("Default connection string not found.");
builder.Services.AddDbContext<WFDBContext>(options => options.UseSqlServer(cnn_str).EnableDetailedErrors());
builder.Services.AddWorkFlow().AddUserManager<WFDBContext>();
builder.Services.AddCookieBasedLocalizer();
builder.ConfigureBySection<DirectorySearchOptions>();
builder.Services.AddDirectorySearchService();
builder.Services.AddJWTService<UserReadDto>(user => new SecurityTokenDescriptor()
try
{
Claims = user.ToClaimList().ToDictionary(claim => claim.Type, claim => claim.Value as object)
});
var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;
builder.Services.AddControllers();
// Add NLogger
builder.Logging.ClearProviders();
builder.Host.UseNLog();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
// Add services to the container.
var cnn_str = config.GetConnectionString("Default") ?? throw new("Default connection string not found.");
builder.Services.AddDbContext<WFDBContext>(options => options.UseSqlServer(cnn_str).EnableDetailedErrors());
builder.Services.AddWorkFlow().AddUserManager<WFDBContext>();
builder.Services.AddCookieBasedLocalizer();
builder.ConfigureBySection<DirectorySearchOptions>();
builder.Services.AddDirectorySearchService();
builder.Services.AddJWTService<UserReadDto>(user => new SecurityTokenDescriptor()
{
options.Cookie.HttpOnly = true; // Makes the cookie inaccessible to client-side scripts for security
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // Ensures cookies are sent over HTTPS only
options.Cookie.SameSite = SameSiteMode.Strict; // Protects against CSRF attacks by restricting how cookies are sent with requests from external sites
options.LoginPath = "/api/auth/login";
options.LogoutPath = "/api/auth/logout";
options.ExpireTimeSpan = TimeSpan.FromMinutes(60); // timeout.
options.SlidingExpiration = true; //refreshes the expiration time on each request.
options.Cookie.Name = "AuthSession";
Claims = user.ToClaimList().ToDictionary(claim => claim.Type, claim => claim.Value as object)
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
bool disableAPIKeyAuth = config.GetValue<bool>("DisableAPIKeyAuth") && builder.IsDevOrDiP();
if (disableAPIKeyAuth)
builder.Services.AddAPIKeyAuth(new APIKeyAuthOptions());
else
if (config.GetSection("APIKeyAuth").Get<APIKeyAuthOptions>() is APIKeyAuthOptions options)
builder.Services.AddAPIKeyAuth(options);
else
throw new("The API Key Authorization configuration is not available in the app settings, even though the app is not in development or DiP mode and API Key Authorization is not disabled.");
var app = builder.Build();
builder.Services.AddControllers();
// Configure the HTTP request pipeline.
if (app.IsDevOrDiP() && app.Configuration.GetValue<bool>("EnableSwagger"))
{
app.UseSwagger();
app.UseSwaggerUI();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.HttpOnly = true; // Makes the cookie inaccessible to client-side scripts for security
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // Ensures cookies are sent over HTTPS only
options.Cookie.SameSite = SameSiteMode.Strict; // Protects against CSRF attacks by restricting how cookies are sent with requests from external sites
options.LoginPath = "/api/auth/login";
options.LogoutPath = "/api/auth/logout";
options.ExpireTimeSpan = TimeSpan.FromMinutes(60); // timeout.
options.SlidingExpiration = true; //refreshes the expiration time on each request.
options.Cookie.Name = "AuthSession";
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(setupAct =>
{
if(!disableAPIKeyAuth)
setupAct.OperationFilter<APIKeyAuthHeaderOpFilter>();
if(config.GetSection("OpenApiInfo").Get<OpenApiInfo>() is OpenApiInfo openApiInfo)
setupAct.SwaggerDoc(openApiInfo?.Version ?? "v1", openApiInfo);
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.IsDevOrDiP() && app.Configuration.GetValue<bool>("EnableSwagger"))
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseCookieBasedLocalizer("de-DE");
app.MapControllers();
app.Run();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseCookieBasedLocalizer("de-DE");
app.MapControllers();
app.Run();
catch (Exception ex)
{
logger.Error(ex, "Stopped program because of exception.");
throw;
}

8
WorkFlow.API/WFKey.cs Normal file
View File

@ -0,0 +1,8 @@
namespace WorkFlow.API
{
public static class WFKey
{
public static readonly string WrongPassword = nameof(WrongPassword);
public static readonly string UserNotFoundOrWrongPassword = nameof(UserNotFoundOrWrongPassword);
}
}

View File

@ -8,6 +8,8 @@
<ItemGroup>
<PackageReference Include="DigitalData.Core.API" Version="2.0.0" />
<PackageReference Include="NLog" Version="5.3.4" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.14" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

View File

@ -1,12 +1,56 @@
{
"DiPMode": true,
"EnableSwagger": true,
"DisableAPIKeyAuth": false,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"NLog": {
"throwConfigExceptions": true,
"variables": {
"logDirectory": "E:\\LogFiles\\Digital Data\\workFlow",
"logFileNamePrefix": "${shortdate}-ECM.EnvelopeGenerator.Web"
},
"targets": {
"infoLogs": {
"type": "File",
"fileName": "${logDirectory}\\${logFileNamePrefix}-Info.log",
"maxArchiveDays": 30
},
"errorLogs": {
"type": "File",
"fileName": "${logDirectory}\\${logFileNamePrefix}-Error.log",
"maxArchiveDays": 30
},
"criticalLogs": {
"type": "File",
"fileName": "${logDirectory}\\${logFileNamePrefix}-Critical.log",
"maxArchiveDays": 30
}
},
// Trace, Debug, Info, Warn, Error and *Fatal*
"rules": [
{
"logger": "*",
"minLevel": "Info",
"maxLevel": "Warn",
"writeTo": "infoLogs"
},
{
"logger": "*",
"level": "Error",
"writeTo": "errorLogs"
},
{
"logger": "*",
"level": "Fatal",
"writeTo": "criticalLogs"
}
]
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
@ -19,5 +63,18 @@
"User": "(&(objectClass=user)(sAMAccountName=*))",
"Group": "(&(objectClass=group) (samAccountName=*))"
}
},
"APIKeyAuth": {
"Key": "ULbcOUiAXAoCXPviyCGtObZUGnrCHNgDmtNbQNpq5MOhB0EFQn18dObdQ93INNy8xIcnOPMJfEHqOotllELVrJ2R5AjqOfQszT2j00w215GanD3UiJGwFhwmdoNFsmNj",
"HeaderName": "X-API-Key",
"SwaggerDescription": "Required header for API key authentication. Enter a valid API key."
},
"OpenApiInfo": {
"Title": "WorkFlow API",
"Contact": {
"Email": "info-flow@digitaldata.works",
"Name": "Digital Data GmbH",
"Url": "https://digitaldata.works/"
}
}
}