feat: Implement API client and token models with hashing and JWT authentication

- Added ApiClientModel and ApiTokenModel for managing API clients and tokens.
- Introduced ConfigurationDefinitionModel and ConfigurationValueModel for configuration management.
- Created CredentialSecretModel for storing credential secrets.
- Developed DeploymentArtifactModel and DeploymentBatchModel for deployment management.
- Enhanced DeploymentTargetModel and DeploymentTemplateSelectionModel to support template revisions.
- Added TemplateRevisionModel and TemplateVersionModel for versioning templates.
- Implemented ApiClientSecretHasher for secure secret hashing.
- Created ApiTokenService for generating and validating JWT tokens.
- Updated QueueJobService to handle deployment requests with artifacts.
- Configured authentication settings in appsettings.json for JWT and Negotiate authentication.
This commit is contained in:
Torsten Brendgen
2026-07-09 14:44:38 +02:00
parent 1aa2adac08
commit dc93ea0813
72 changed files with 32906 additions and 516 deletions

View File

@@ -0,0 +1,94 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.SelfService.Portal.Core.API.Context;
using Microsoft.SelfService.Portal.Core.API.Dto.Auth;
using Microsoft.SelfService.Portal.Core.API.Models;
using Microsoft.SelfService.Portal.Core.API.Services;
using System.Text.Json;
namespace Microsoft.SelfService.Portal.Core.API.Controllers
{
[Route("api/auth")]
[ApiController]
public class AuthController : Controller
{
private readonly DataContext _context;
private readonly ApiTokenService _tokenService;
public AuthController(DataContext context, ApiTokenService tokenService)
{
_context = context;
_tokenService = tokenService;
}
[HttpPost("token")]
[AllowAnonymous]
[ProducesResponseType(200, Type = typeof(TokenResponseDto))]
[ProducesResponseType(400)]
[ProducesResponseType(401)]
public IActionResult CreateToken([FromBody] TokenRequestDto request)
{
if (request == null
|| string.IsNullOrWhiteSpace(request.ClientId)
|| string.IsNullOrWhiteSpace(request.ClientSecret))
{
return BadRequest(new { message = "ClientId and ClientSecret are required." });
}
var client = _context.ApiClients
.FirstOrDefault(existing => existing.ClientId == request.ClientId);
if (client == null
|| !client.IsEnabled
|| (client.ExpiresAt.HasValue && client.ExpiresAt.Value <= DateTime.UtcNow)
|| !ApiClientSecretHasher.VerifySecret(request.ClientSecret, client.SecretHash))
{
return Unauthorized(new { message = "Invalid client credentials." });
}
IReadOnlyCollection<string> scopes;
try
{
scopes = _tokenService.ResolveRequestedScopes(client, request.Scope);
}
catch (InvalidOperationException ex)
{
return Unauthorized(new { message = ex.Message });
}
var token = _tokenService.CreateAccessToken(client, scopes);
var now = DateTime.UtcNow;
client.LastUsedAt = now;
client.Modified = now;
client.ModifiedBy = client.ClientId;
_context.ApiTokens.Add(new ApiTokenModel
{
Id = Guid.NewGuid(),
Jti = token.Jti,
Subject = client.ClientId,
SubjectType = "ApiClient",
ApiClientId = client.Id,
Name = $"Client credentials token for {client.Name}",
ScopesJson = JsonSerializer.Serialize(scopes),
IssuedAt = token.IssuedAt,
ExpiresAt = token.ExpiresAt,
Created = now,
CreatedBy = client.ClientId,
Modified = now,
ModifiedBy = client.ClientId
});
_context.SaveChanges();
return Ok(new TokenResponseDto
{
AccessToken = token.AccessToken,
ExpiresIn = _tokenService.TokenLifetimeSeconds,
Scope = string.Join(' ', scopes)
});
}
}
}