Compare commits

...

5 Commits

9 changed files with 129 additions and 174 deletions

View File

@ -1,154 +0,0 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using DigitalData.UserManager.Application.Contracts;
using DigitalData.UserManager.Application.DTOs.User;
using Microsoft.AspNetCore.Authorization;
using DigitalData.UserManager.Application;
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, IDirectorySearchService dirSearchService, IStringLocalizer<Resource> localizer, ILogger<AuthController> logger) : ControllerBase
{
[AllowAnonymous]
[HttpGet("check")]
public IActionResult CheckAuthentication()
{
try
{
return Ok(User.Identity?.IsAuthenticated ?? false);
}
catch(Exception ex)
{
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
{
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);
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);
}
}
[Authorize]
[HttpGet("user")]
public async Task<IActionResult> GetUserWithClaims()
{
try
{
// Extract the username from the Name claim.
string? username = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
if (string.IsNullOrEmpty(username))
return Unauthorized();
return await userService.ReadByUsernameAsync(username)
.ThenAsync(Ok, IActionResult (m, n) =>
{
logger.LogNotice(n);
return NotFound(Result.Fail().Message(localizer[Key.UserNotFound].Value));
});
}
catch (Exception ex)
{
logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[Authorize]
[HttpPost("logout")]
public async Task<IActionResult> Logout()
{
try
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok();
}
catch(Exception ex)
{
logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
}

View File

@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using WorkFlow.API.Models;
using WorkFlow.API.Attributes;
namespace WorkFlow.API.Controllers;
//TODO: implement up-to-date AuthController in UserManager
[APIKeyAuth]
[Route("api/Auth")]
[ApiController]
[Tags("Auth")]
public class PlaceholderAuthController : ControllerBase
{
[HttpPost]
public IActionResult CreateTokenViaBody([FromBody] Login login)
{
throw new NotImplementedException();
}
[HttpGet("check")]
[Authorize]
public IActionResult Check() => Ok();
}

View File

@ -0,0 +1,18 @@
namespace WorkFlow.API;
public class LazyServiceProvider : IServiceProvider
{
private Lazy<IServiceProvider>? _serviceProvider;
public Func<IServiceProvider> Factory
{
set => _serviceProvider = new(value);
}
public object? GetService(Type serviceType)
{
if (_serviceProvider is null)
throw new InvalidOperationException("GetService cannot be called before _serviceProvider is set.");
return _serviceProvider.Value.GetService(serviceType);
}
}

View File

@ -0,0 +1,12 @@
namespace WorkFlow.API.Models;
public class AuthTokenKeys
{
public string Cookie { get; init; } = "AuthToken";
public string QueryString { get; init; } = "AuthToken";
public string Issuer { get; init; } = "auth.digitaldata.works";
public string Audience { get; init; } = "work-flow.digitaldata.works";
}

View File

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

View File

@ -2,7 +2,6 @@ using WorkFlow.Application;
using DigitalData.UserManager.Application;
using Microsoft.EntityFrameworkCore;
using WorkFlow.Infrastructure;
using Microsoft.AspNetCore.Authentication.Cookies;
using DigitalData.Core.API;
using DigitalData.Core.Application;
using DigitalData.UserManager.Application.DTOs.User;
@ -14,6 +13,10 @@ using WorkFlow.API.Extensions;
using WorkFlow.API.Filters;
using Microsoft.OpenApi.Models;
using DigitalData.Auth.Client;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using WorkFlow.API;
using Microsoft.Extensions.Options;
using DigitalData.Core.Abstractions.Security;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized.");
@ -47,33 +50,80 @@ try
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.");
// Created separately from AuthClientParams (added via options) for use in Jwt Bearer configuration
var authPublicKey = config.GetSection("AuthPublicKey").Get<ClientPublicKey>() ?? throw new InvalidOperationException("The AuthPublicKey configuration is missing or invalid.");
var lazyProvider = new LazyServiceProvider();
builder.Services.AddAuthHubClient(config.GetSection("AuthClientParams"), opt => opt.PublicKeys.Add(authPublicKey));
builder.Services.AddAuthHubClient(config.GetSection("AuthClientParams"));
builder.Services.AddControllers();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
var authTokenKeys = config.GetSection(nameof(AuthTokenKeys)).Get<AuthTokenKeys>() ?? new();
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) =>
{
return [authPublicKey.SecurityKey];
var clientParams = lazyProvider.GetRequiredService<IOptions<ClientParams>>()?.Value;
var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience);
return [publicKey.SecurityKey];
},
ValidateIssuer = true,
ValidIssuer = authPublicKey.Issuer,
ValidIssuer = authTokenKeys.Issuer,
ValidateAudience = true,
ValidAudience = authPublicKey.Audience
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;
}
};
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(setupAct =>
{
setupAct.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Scheme = "Bearer"
});
setupAct.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
if (!disableAPIKeyAuth)
setupAct.OperationFilter<APIKeyAuthHeaderOpFilter>();
@ -83,6 +133,8 @@ try
var app = builder.Build();
lazyProvider.Factory = () => app.Services;
// Configure the HTTP request pipeline.
if (app.IsDevOrDiP() && app.Configuration.GetValue<bool>("EnableSwagger"))
{

View File

@ -12,7 +12,7 @@
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5130",
"environmentVariables": {
@ -22,7 +22,7 @@
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7120;http://localhost:5130",
"environmentVariables": {
@ -31,7 +31,7 @@
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchBrowser": false,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"

View File

@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DigitalData.Auth.Client" Version="1.1.5" />
<PackageReference Include="DigitalData.Auth.Client" Version="1.2.1.1" />
<PackageReference Include="DigitalData.Core.API" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.13" />
<PackageReference Include="NLog" Version="5.3.4" />

View File

@ -78,11 +78,14 @@
}
},
"AuthClientParams": {
"Url": "https://localhost:7192",
"PublicKeys": []
},
"AuthPublicKey": {
"Issuer": "auth.digitaldata.works",
"Audience": "work-flow.digitaldata.works"
"Url": "https://localhost:7192/auth-hub",
"PublicKeys": [
{
"Issuer": "auth.digitaldata.works",
"Audience": "work-flow.digitaldata.works",
"Content": "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3QCd7dH/xOUITFZbitMa/xnh8a0LyL6ZBvSRAwkI9ceplTRSHJXoM1oB+xtjWE1kOuHVLe941Tm03szS4+/rHIm0Ejva/KKlv7sPFAHE/pWuoPS303vOHgI4HAFcuwywA8CghUWzaaK5LU/Hl8srWwxBHv5hKIUjJFJygeAIENvFOZ1gFbB3MPEC99PiPOwAmfl4tMQUmSsFyspl/RWVi7bTv26ZE+m3KPcWppmvmYjXlSitxRaySxnfFvpca/qWfd/uUUg2KWKtpAwWVkqr0qD9v3TyKSgHoGDsrFpwSx8qufUJSinmZ1u/0iKl6TXeHubYS4C4SUSVjOWXymI2ZQIDAQAB-----END PUBLIC KEY-----"
}
],
"RetryDelay": "00:00:05"
}
}