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

@@ -1,11 +1,14 @@
using Microsoft.AspNetCore.Authentication.Negotiate;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.Negotiate;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.SelfService.Portal.Core.API.Context;
using Microsoft.SelfService.Portal.Core.API.Interfaces;
using Microsoft.SelfService.Portal.Core.API.Repository;
using Microsoft.SelfService.Portal.Core.API.Services;
using Microsoft.Extensions.FileProviders;
using System.IdentityModel.Tokens.Jwt;
using System.Text.Json.Serialization;
@@ -27,6 +30,7 @@ builder.Services.AddScoped<IDeploymentInterface, DeploymentRepository>();
builder.Services.AddScoped<ITemplateInterface, TemplateRepository>();
builder.Services.AddScoped<ITemplateCategoryInterface, TemplateCategoryRepository>();
builder.Services.AddScoped<IQueueJobService, QueueJobService>();
builder.Services.AddScoped<ApiTokenService>();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
@@ -36,8 +40,72 @@ builder.Services.AddAutoMapper(_ => { }, AppDomain.CurrentDomain.GetAssemblies()
builder.Services.AddDbContext<DataContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Context") ?? throw new InvalidOperationException("Connection string 'Context' not found.")));
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.AddNegotiate();
var tokenService = new ApiTokenService(builder.Configuration);
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "Smart";
options.DefaultChallengeScheme = "Smart";
})
.AddPolicyScheme("Smart", "Negotiate or Bearer", options =>
{
options.ForwardDefaultSelector = context =>
{
var authorization = context.Request.Headers.Authorization.ToString();
return authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
? JwtBearerDefaults.AuthenticationScheme
: NegotiateDefaults.AuthenticationScheme;
};
})
.AddNegotiate()
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = tokenService.Issuer,
ValidateAudience = true,
ValidAudience = tokenService.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = tokenService.GetSigningKey(),
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(2)
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var jti = context.Principal?.FindFirst(JwtRegisteredClaimNames.Jti)?.Value;
if (string.IsNullOrWhiteSpace(jti))
{
context.Fail("Token does not contain a token id.");
return Task.CompletedTask;
}
var dataContext = context.HttpContext.RequestServices.GetRequiredService<DataContext>();
var token = dataContext.ApiTokens.FirstOrDefault(existing => existing.Jti == jti);
if (token == null)
{
context.Fail("Token is not registered.");
return Task.CompletedTask;
}
if (token.RevokedAt.HasValue || token.ExpiresAt <= DateTime.UtcNow)
{
context.Fail("Token is revoked or expired.");
return Task.CompletedTask;
}
token.LastUsedAt = DateTime.UtcNow;
token.Modified = DateTime.UtcNow;
token.ModifiedBy = "BearerToken";
dataContext.SaveChanges();
return Task.CompletedTask;
}
};
});
builder.Services.AddHttpContextAccessor();
@@ -45,6 +113,15 @@ builder.Services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy.
options.FallbackPolicy = options.DefaultPolicy;
options.AddPolicy("QueueProcess", policy => policy.RequireClaim("scope", "queue.process"));
options.AddPolicy("DeploymentRead", policy => policy.RequireClaim("scope", "deployment.read"));
options.AddPolicy("CredentialResolve", policy => policy.RequireClaim("scope", "credential.resolve"));
options.AddPolicy("TokenManage", policy => policy.RequireAssertion(context =>
context.User.HasClaim("scope", "token.manage")
|| context.User.Identity?.AuthenticationType == NegotiateDefaults.AuthenticationScheme));
options.AddPolicy("TokenAdmin", policy => policy.RequireAssertion(context =>
context.User.HasClaim("scope", "token.admin")
|| context.User.Identity?.AuthenticationType == NegotiateDefaults.AuthenticationScheme));
});
var app = builder.Build();
@@ -101,3 +178,5 @@ if (Directory.Exists(frontendDistPath))
app.Run();