diff --git a/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj b/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj index f032eed8..73ba45fe 100644 --- a/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj +++ b/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj @@ -13,7 +13,7 @@ - + diff --git a/EnvelopeGenerator.Domain/EnvelopeGenerator.Domain.csproj b/EnvelopeGenerator.Domain/EnvelopeGenerator.Domain.csproj index 8a430d70..97b0ca90 100644 --- a/EnvelopeGenerator.Domain/EnvelopeGenerator.Domain.csproj +++ b/EnvelopeGenerator.Domain/EnvelopeGenerator.Domain.csproj @@ -7,7 +7,7 @@ - + diff --git a/EnvelopeGenerator.GeneratorAPI/Controllers/AuthController.cs b/EnvelopeGenerator.GeneratorAPI/Controllers/AuthController.cs index 5ce1ce7a..a5e8f748 100644 --- a/EnvelopeGenerator.GeneratorAPI/Controllers/AuthController.cs +++ b/EnvelopeGenerator.GeneratorAPI/Controllers/AuthController.cs @@ -1,201 +1,150 @@ using DigitalData.Core.Abstractions.Application; using DigitalData.UserManager.Application.Contracts; -using DigitalData.UserManager.Application.DTOs.User; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; -using System.Security.Claims; -using DigitalData.UserManager.Application.DTOs.Auth; using Microsoft.AspNetCore.Authorization; using EnvelopeGenerator.GeneratorAPI.Models; -namespace EnvelopeGenerator.GeneratorAPI.Controllers +namespace EnvelopeGenerator.GeneratorAPI.Controllers; + +/// +/// Controller verantwortlich für die Benutzer-Authentifizierung, einschließlich Anmelden, Abmelden und Überprüfung des Authentifizierungsstatus. +/// +[Route("api/[controller]")] +[ApiController] +public partial class AuthController : ControllerBase { + private readonly ILogger _logger; + private readonly IUserService _userService; + private readonly IDirectorySearchService _dirSearchService; + /// - /// Controller verantwortlich für die Benutzer-Authentifizierung, einschließlich Anmelden, Abmelden und Überprüfung des Authentifizierungsstatus. + /// Initializes a new instance of the class. /// - [Route("api/[controller]")] - [ApiController] - public partial class AuthController : ControllerBase + /// The logger instance. + /// The user service instance. + /// The directory search service instance. + public AuthController(ILogger logger, IUserService userService, IDirectorySearchService dirSearchService) { - private readonly ILogger _logger; - private readonly IUserService _userService; - private readonly IDirectorySearchService _dirSearchService; - - /// - /// Initializes a new instance of the class. - /// - /// The logger instance. - /// The user service instance. - /// The directory search service instance. - public AuthController(ILogger logger, IUserService userService, IDirectorySearchService dirSearchService) - { - _logger = logger; - _userService = userService; - _dirSearchService = dirSearchService; - } - - /// - /// Authentifiziert einen Benutzer und generiert ein JWT-Token. Wenn 'cookie' wahr ist, wird das Token als HTTP-Only-Cookie zurückgegeben. - /// - /// Benutzeranmeldedaten (Benutzername und Passwort). - /// Wenn wahr, wird das JWT-Token auch als HTTP-Only-Cookie gesendet. - /// - /// Gibt eine HTTP 200 oder 401. - /// - /// - /// Sample request: - /// - /// POST /api/auth?cookie=true - /// { - /// "username": "MaxMustermann", - /// "password": "Geheim123!" - /// } - /// - /// POST /api/auth?cookie=true - /// { - /// "id": "1", - /// "password": "Geheim123!" - /// } - /// - /// - /// Erfolgreiche Anmeldung. Gibt das JWT-Token im Antwortkörper oder als Cookie zurück, wenn 'cookie' wahr ist. - /// Unbefugt. Ungültiger Benutzername oder Passwort. - [ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")] - [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] - [AllowAnonymous] - [HttpPost] - public async Task Login([FromBody] Login login, [FromQuery] bool cookie = false) - { - try - { - 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 Forbid(); - } - - UserReadDto user = uRes.Data; - - // Create claims - var claims = new List - { - new (ClaimTypes.NameIdentifier, user.Id.ToString()), - new (ClaimTypes.Name, user.Username), - new (ClaimTypes.Surname, user.Name!), - new (ClaimTypes.GivenName, user.Prename!), - new (ClaimTypes.Email, user.Email!), - }; - - // Create claimsIdentity - var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); - - // Create authProperties - var authProperties = new AuthenticationProperties - { - IsPersistent = true, - AllowRefresh = true, - ExpiresUtc = DateTime.Now.AddMinutes(180) - }; - - // Sign in - await HttpContext.SignInAsync( - CookieAuthenticationDefaults.AuthenticationScheme, - new ClaimsPrincipal(claimsIdentity), - authProperties); - - return Ok(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unexpected error occurred.\n{ErrorMessage}", ex.Message); - return StatusCode(StatusCodes.Status500InternalServerError); - } - } - - /// - /// Authentifiziert einen Benutzer und generiert ein JWT-Token. Das Token wird als HTTP-only-Cookie zurückgegeben. - /// - /// Benutzeranmeldedaten (Benutzername und Passwort). - /// - /// Gibt eine HTTP 200 oder 401. - /// - /// - /// Sample request: - /// - /// POST /api/auth/form - /// { - /// "username": "MaxMustermann", - /// "password": "Geheim123!" - /// } - /// - /// - /// Erfolgreiche Anmeldung. Gibt das JWT-Token im Antwortkörper oder als Cookie zurück, wenn 'cookie' wahr ist. - /// Unbefugt. Ungültiger Benutzername oder Passwort. - [ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")] - [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] - [AllowAnonymous] - [HttpPost] - [Route("form")] - public async Task Login([FromForm] Login login) - { - return await Login(login, true); - } - - /// - /// Entfernt das Authentifizierungs-Cookie des Benutzers (AuthCookie) - /// - /// - /// Gibt eine HTTP 200 oder 401. - /// - /// - /// Sample request: - /// - /// POST /api/auth/logout - /// - /// - /// Erfolgreich gelöscht, wenn der Benutzer ein berechtigtes Cookie hat. - /// Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben. - [ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")] - [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] - [Authorize] - [HttpPost("logout")] - public async Task Logout() - { - try - { - await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - return Ok(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unexpected error occurred.\n{ErrorMessage}", ex.Message); - return StatusCode(StatusCodes.Status500InternalServerError); - } - } - - /// - /// Prüft, ob der Benutzer ein autorisiertes Token hat. - /// - /// Wenn ein autorisiertes Token vorhanden ist HTTP 200 asynchron 401 - /// - /// Sample request: - /// - /// GET /api/auth - /// - /// - /// Wenn es einen autorisierten Cookie gibt. - /// Wenn kein Cookie vorhanden ist oder nicht autorisierte. - [ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")] - [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] - [Authorize] - [HttpGet] - public IActionResult IsAuthenticated() => Ok(); + _logger = logger; + _userService = userService; + _dirSearchService = dirSearchService; } + + /// + /// Authentifiziert einen Benutzer und generiert ein JWT-Token. Wenn 'cookie' wahr ist, wird das Token als HTTP-Only-Cookie zurückgegeben. + /// + /// Benutzeranmeldedaten (Benutzername und Passwort). + /// Wenn wahr, wird das JWT-Token auch als HTTP-Only-Cookie gesendet. + /// + /// Gibt eine HTTP 200 oder 401. + /// + /// + /// Sample request: + /// + /// POST /api/auth?cookie=true + /// { + /// "username": "MaxMustermann", + /// "password": "Geheim123!" + /// } + /// + /// POST /api/auth?cookie=true + /// { + /// "id": "1", + /// "password": "Geheim123!" + /// } + /// + /// + /// Erfolgreiche Anmeldung. Gibt das JWT-Token im Antwortkörper oder als Cookie zurück, wenn 'cookie' wahr ist. + /// Unbefugt. Ungültiger Benutzername oder Passwort. + [ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + [AllowAnonymous] + [HttpPost] + public Task Login([FromBody] Login login, [FromQuery] bool cookie = false) + { + // added to configure open API (swagger and scalar) + throw new NotImplementedException(); + } + + /// + /// Authentifiziert einen Benutzer und generiert ein JWT-Token. Das Token wird als HTTP-only-Cookie zurückgegeben. + /// + /// Benutzeranmeldedaten (Benutzername und Passwort). + /// + /// Gibt eine HTTP 200 oder 401. + /// + /// + /// Sample request: + /// + /// POST /api/auth/form + /// { + /// "username": "MaxMustermann", + /// "password": "Geheim123!" + /// } + /// + /// + /// Erfolgreiche Anmeldung. Gibt das JWT-Token im Antwortkörper oder als Cookie zurück, wenn 'cookie' wahr ist. + /// Unbefugt. Ungültiger Benutzername oder Passwort. + [ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + [AllowAnonymous] + [HttpPost] + [Route("form")] + public Task Login([FromForm] Login login) + { + // added to configure open API (swagger and scalar) + throw new NotImplementedException(); + } + + /// + /// Entfernt das Authentifizierungs-Cookie des Benutzers (AuthCookie) + /// + /// + /// Gibt eine HTTP 200 oder 401. + /// + /// + /// Sample request: + /// + /// POST /api/auth/logout + /// + /// + /// Erfolgreich gelöscht, wenn der Benutzer ein berechtigtes Cookie hat. + /// Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben. + [ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + [Authorize] + [HttpPost("logout")] + public async Task Logout() + { + try + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + return Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error occurred.\n{ErrorMessage}", ex.Message); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + /// + /// Prüft, ob der Benutzer ein autorisiertes Token hat. + /// + /// Wenn ein autorisiertes Token vorhanden ist HTTP 200 asynchron 401 + /// + /// Sample request: + /// + /// GET /api/auth + /// + /// + /// Wenn es einen autorisierten Cookie gibt. + /// Wenn kein Cookie vorhanden ist oder nicht autorisierte. + [ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + [Authorize] + [HttpGet] + public IActionResult IsAuthenticated() => Ok(); } \ No newline at end of file diff --git a/EnvelopeGenerator.GeneratorAPI/EnvelopeGenerator.GeneratorAPI.csproj b/EnvelopeGenerator.GeneratorAPI/EnvelopeGenerator.GeneratorAPI.csproj index e76957a6..6e922bad 100644 --- a/EnvelopeGenerator.GeneratorAPI/EnvelopeGenerator.GeneratorAPI.csproj +++ b/EnvelopeGenerator.GeneratorAPI/EnvelopeGenerator.GeneratorAPI.csproj @@ -19,12 +19,13 @@ + - + - - - + + + diff --git a/EnvelopeGenerator.GeneratorAPI/Jenkinsfile b/EnvelopeGenerator.GeneratorAPI/Jenkinsfile new file mode 100644 index 00000000..3546ad67 --- /dev/null +++ b/EnvelopeGenerator.GeneratorAPI/Jenkinsfile @@ -0,0 +1,10 @@ +pipeline { + agent any + stages { + stage('Build') { + steps { + sh 'dotnet build' + } + } + } +} diff --git a/EnvelopeGenerator.GeneratorAPI/Models/AuthTokenKeys.cs b/EnvelopeGenerator.GeneratorAPI/Models/AuthTokenKeys.cs new file mode 100644 index 00000000..75bf927a --- /dev/null +++ b/EnvelopeGenerator.GeneratorAPI/Models/AuthTokenKeys.cs @@ -0,0 +1,28 @@ +namespace EnvelopeGenerator.GeneratorAPI.Models; + +/// +/// Represents the keys and default values used for authentication token handling +/// within the Envelope Generator API. +/// +public class AuthTokenKeys +{ + /// + /// Gets the name of the cookie used to store the authentication token. + /// + public string Cookie { get; init; } = "AuthToken"; + + /// + /// Gets the name of the query string parameter used to pass the authentication token. + /// + public string QueryString { get; init; } = "AuthToken"; + + /// + /// Gets the expected issuer value for the authentication token. + /// + public string Issuer { get; init; } = "auth.digitaldata.works"; + + /// + /// Gets the expected audience value for the authentication token. + /// + public string Audience { get; init; } = "sign-flow-gen.digitaldata.works"; +} diff --git a/EnvelopeGenerator.GeneratorAPI/Program.cs b/EnvelopeGenerator.GeneratorAPI/Program.cs index 77ee0bab..bfdb024b 100644 --- a/EnvelopeGenerator.GeneratorAPI/Program.cs +++ b/EnvelopeGenerator.GeneratorAPI/Program.cs @@ -1,6 +1,5 @@ using DigitalData.Core.API; using DigitalData.Core.Application; -using DigitalData.UserManager.Application; using EnvelopeGenerator.Infrastructure; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Localization; @@ -10,11 +9,19 @@ using Scalar.AspNetCore; using Microsoft.OpenApi.Models; using DigitalData.UserManager.DependencyInjection; using EnvelopeGenerator.Application; +using DigitalData.Auth.Client; +using DigitalData.Core.Abstractions; +using EnvelopeGenerator.GeneratorAPI.Models; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using DigitalData.Core.Abstractions.Security.Extensions; var builder = WebApplication.CreateBuilder(args); var config = builder.Configuration; +var deferredProvider = new DeferredServiceProvider(); + builder.Services.AddControllers(); //CORS Policy @@ -85,6 +92,49 @@ builder.Services.AddOpenApi(); var connStr = config.GetConnectionString("Default") ?? throw new InvalidOperationException("There is no default connection string in appsettings.json."); builder.Services.AddDbContext(options => options.UseSqlServer(connStr)); +builder.Services.AddAuthHubClient(config.GetSection("AuthClientParams")); + +var authTokenKeys = config.GetOrDefault(); + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}) + .AddJwtBearer(opt => + { + opt.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) => + { + var clientParams = deferredProvider.GetOptions(); + var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience); + return new List() { publicKey.SecurityKey }; + }, + ValidateIssuer = true, + ValidIssuer = authTokenKeys.Issuer, + ValidateAudience = true, + ValidAudience = authTokenKeys.Audience, + }; + + opt.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + // if there is no token read related cookie or query string + if (context.Token is null) // if there is no token + { + if (context.Request.Cookies.TryGetValue(authTokenKeys.Cookie, out var cookieToken) && cookieToken is not null) + context.Token = cookieToken; + else if (context.Request.Query.TryGetValue(authTokenKeys.QueryString, out var queryStrToken)) + context.Token = queryStrToken; + } + return Task.CompletedTask; + } + }; + }); + // Authentication builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => @@ -114,6 +164,8 @@ builder.Services var app = builder.Build(); +deferredProvider.Factory = () => app.Services; + app.MapOpenApi(); // Configure the HTTP request pipeline. diff --git a/EnvelopeGenerator.GeneratorAPI/appsettings.Development.json b/EnvelopeGenerator.GeneratorAPI/appsettings.Development.json index 0c208ae9..7525e080 100644 --- a/EnvelopeGenerator.GeneratorAPI/appsettings.Development.json +++ b/EnvelopeGenerator.GeneratorAPI/appsettings.Development.json @@ -4,5 +4,15 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "AuthClientParams": { + "Url": "https://localhost:7192/auth-hub", + "PublicKeys": [ + { + "Issuer": "auth.digitaldata.works", + "Audience": "sign-flow-gen.digitaldata.works" + } + ], + "RetryDelay": "00:00:05" } } diff --git a/EnvelopeGenerator.GeneratorAPI/appsettings.json b/EnvelopeGenerator.GeneratorAPI/appsettings.json index 734a3dcb..c03dbb20 100644 --- a/EnvelopeGenerator.GeneratorAPI/appsettings.json +++ b/EnvelopeGenerator.GeneratorAPI/appsettings.json @@ -20,5 +20,21 @@ "User": "(&(objectClass=user)(sAMAccountName=*))", "Group": "(&(objectClass=group)(samAccountName=*))" } + }, + "AuthClientParams": { + "Url": "https://localhost:7192/auth-hub", + "PublicKeys": [ + { + "Issuer": "auth.digitaldata.works", + "Audience": "sign-flow-gen.digitaldata.works" + } + ], + "RetryDelay": "00:00:05" + }, + "AuthTokenKeys": { + "Cookie": "AuthToken", + "QueryString": "AuthToken", + "Issuer": "auth.digitaldata.works", + "Audience": "work-flow.digitaldata.works" } } diff --git a/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj b/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj index f18a5f72..f93f27c1 100644 --- a/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj +++ b/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj @@ -7,7 +7,7 @@ - + diff --git a/EnvelopeGenerator.Terminal/EnvelopeGenerator.Terminal.csproj b/EnvelopeGenerator.Terminal/EnvelopeGenerator.Terminal.csproj index 175074cf..0fea2531 100644 --- a/EnvelopeGenerator.Terminal/EnvelopeGenerator.Terminal.csproj +++ b/EnvelopeGenerator.Terminal/EnvelopeGenerator.Terminal.csproj @@ -19,7 +19,7 @@ - + diff --git a/EnvelopeGenerator.Tests.Application/EnvelopeGenerator.Tests.Application.csproj b/EnvelopeGenerator.Tests.Application/EnvelopeGenerator.Tests.Application.csproj index c6e028c7..1c549a9b 100644 --- a/EnvelopeGenerator.Tests.Application/EnvelopeGenerator.Tests.Application.csproj +++ b/EnvelopeGenerator.Tests.Application/EnvelopeGenerator.Tests.Application.csproj @@ -23,7 +23,7 @@ - + diff --git a/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj b/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj index 761e4780..dbbfc909 100644 --- a/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj +++ b/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj @@ -2101,7 +2101,7 @@ - +