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:
221
Controllers/ApiTokenController.cs
Normal file
221
Controllers/ApiTokenController.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
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.Security.Claims;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.SelfService.Portal.Core.API.Controllers
|
||||
{
|
||||
[Route("api/tokens")]
|
||||
[ApiController]
|
||||
public class ApiTokenController : Controller
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly ApiTokenService _tokenService;
|
||||
|
||||
public ApiTokenController(DataContext context, ApiTokenService tokenService)
|
||||
{
|
||||
_context = context;
|
||||
_tokenService = tokenService;
|
||||
}
|
||||
|
||||
[HttpGet("my")]
|
||||
[Authorize(Policy = "TokenManage")]
|
||||
[ProducesResponseType(200, Type = typeof(IEnumerable<GetManagedTokenDto>))]
|
||||
public IActionResult GetMyTokens()
|
||||
{
|
||||
var subject = GetCurrentSubject();
|
||||
var subjectType = GetCurrentSubjectType();
|
||||
|
||||
var tokens = _context.ApiTokens
|
||||
.Include(token => token.ApiClient)
|
||||
.Where(token => token.Subject == subject && token.SubjectType == subjectType)
|
||||
.OrderByDescending(token => token.IssuedAt)
|
||||
.ToList();
|
||||
|
||||
return Ok(tokens.Select(ToDto));
|
||||
}
|
||||
|
||||
[HttpPost("my")]
|
||||
[Authorize(Policy = "TokenManage")]
|
||||
[ProducesResponseType(200, Type = typeof(CreateManagedTokenResponseDto))]
|
||||
[ProducesResponseType(400)]
|
||||
public IActionResult CreateMyToken([FromBody] CreateManagedTokenRequestDto request)
|
||||
{
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.Scope))
|
||||
{
|
||||
return BadRequest(new { message = "Scope is required." });
|
||||
}
|
||||
|
||||
IReadOnlyCollection<string> scopes;
|
||||
try
|
||||
{
|
||||
scopes = _tokenService.ResolveRequestedUserScopes(request.Scope);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new { message = ex.Message });
|
||||
}
|
||||
|
||||
var subject = GetCurrentSubject();
|
||||
var subjectType = GetCurrentSubjectType();
|
||||
var name = string.IsNullOrWhiteSpace(request.Name)
|
||||
? $"Token for {subject}"
|
||||
: request.Name.Trim();
|
||||
|
||||
var token = _tokenService.CreateAccessToken(
|
||||
subject,
|
||||
subjectType == "ApiClient" ? subject : null,
|
||||
scopes);
|
||||
var now = DateTime.UtcNow;
|
||||
var expiresAt = request.ExpiresAt.HasValue && request.ExpiresAt.Value < token.ExpiresAt
|
||||
? request.ExpiresAt.Value
|
||||
: token.ExpiresAt;
|
||||
|
||||
if (expiresAt <= now)
|
||||
{
|
||||
return BadRequest(new { message = "ExpiresAt must be in the future." });
|
||||
}
|
||||
|
||||
var model = new ApiTokenModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Jti = token.Jti,
|
||||
Subject = subject,
|
||||
SubjectType = subjectType,
|
||||
Name = name,
|
||||
ScopesJson = JsonSerializer.Serialize(scopes),
|
||||
IssuedAt = token.IssuedAt,
|
||||
ExpiresAt = expiresAt,
|
||||
Created = now,
|
||||
CreatedBy = subject,
|
||||
Modified = now,
|
||||
ModifiedBy = subject
|
||||
};
|
||||
|
||||
_context.ApiTokens.Add(model);
|
||||
_context.SaveChanges();
|
||||
|
||||
return Ok(new CreateManagedTokenResponseDto
|
||||
{
|
||||
Id = model.Id,
|
||||
AccessToken = token.AccessToken,
|
||||
ExpiresIn = Math.Max(0, Convert.ToInt32((expiresAt - now).TotalSeconds)),
|
||||
ExpiresAt = expiresAt,
|
||||
Scope = string.Join(' ', scopes)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpDelete("my/{id}")]
|
||||
[Authorize(Policy = "TokenManage")]
|
||||
[ProducesResponseType(200, Type = typeof(GetManagedTokenDto))]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult RevokeMyToken(Guid id)
|
||||
{
|
||||
var subject = GetCurrentSubject();
|
||||
var subjectType = GetCurrentSubjectType();
|
||||
var token = _context.ApiTokens.FirstOrDefault(existing =>
|
||||
existing.Id == id
|
||||
&& existing.Subject == subject
|
||||
&& existing.SubjectType == subjectType);
|
||||
|
||||
if (token == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
RevokeToken(token, subject);
|
||||
_context.SaveChanges();
|
||||
|
||||
return Ok(ToDto(token));
|
||||
}
|
||||
|
||||
[HttpGet("admin")]
|
||||
[Authorize(Policy = "TokenAdmin")]
|
||||
[ProducesResponseType(200, Type = typeof(IEnumerable<GetManagedTokenDto>))]
|
||||
public IActionResult GetAllTokens()
|
||||
{
|
||||
var tokens = _context.ApiTokens
|
||||
.Include(token => token.ApiClient)
|
||||
.OrderByDescending(token => token.IssuedAt)
|
||||
.ToList();
|
||||
|
||||
return Ok(tokens.Select(ToDto));
|
||||
}
|
||||
|
||||
[HttpDelete("admin/{id}")]
|
||||
[Authorize(Policy = "TokenAdmin")]
|
||||
[ProducesResponseType(200, Type = typeof(GetManagedTokenDto))]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult RevokeTokenAsAdmin(Guid id)
|
||||
{
|
||||
var token = _context.ApiTokens.FirstOrDefault(existing => existing.Id == id);
|
||||
if (token == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
RevokeToken(token, GetCurrentSubject());
|
||||
_context.SaveChanges();
|
||||
|
||||
return Ok(ToDto(token));
|
||||
}
|
||||
|
||||
[HttpGet("scopes")]
|
||||
[Authorize(Policy = "TokenManage")]
|
||||
[ProducesResponseType(200, Type = typeof(IEnumerable<string>))]
|
||||
public IActionResult GetAllowedScopes()
|
||||
{
|
||||
return Ok(_tokenService.ReadAllowedTokenScopes());
|
||||
}
|
||||
|
||||
private string GetCurrentSubject()
|
||||
{
|
||||
return User.FindFirstValue("client_id")
|
||||
?? User.FindFirstValue(ClaimTypes.Name)
|
||||
?? User.Identity?.Name
|
||||
?? "Unknown";
|
||||
}
|
||||
|
||||
private string GetCurrentSubjectType()
|
||||
{
|
||||
return User.HasClaim(claim => claim.Type == "client_id") ? "ApiClient" : "User";
|
||||
}
|
||||
|
||||
private static void RevokeToken(ApiTokenModel token, string revokedBy)
|
||||
{
|
||||
if (!token.RevokedAt.HasValue)
|
||||
{
|
||||
token.RevokedAt = DateTime.UtcNow;
|
||||
token.RevokedBy = revokedBy;
|
||||
token.Modified = DateTime.UtcNow;
|
||||
token.ModifiedBy = revokedBy;
|
||||
}
|
||||
}
|
||||
|
||||
private static GetManagedTokenDto ToDto(ApiTokenModel token)
|
||||
{
|
||||
var scopes = JsonSerializer.Deserialize<string[]>(token.ScopesJson) ?? Array.Empty<string>();
|
||||
|
||||
return new GetManagedTokenDto
|
||||
{
|
||||
Id = token.Id,
|
||||
Subject = token.Subject,
|
||||
SubjectType = token.SubjectType,
|
||||
ClientId = token.ApiClient?.ClientId,
|
||||
Name = token.Name,
|
||||
Scope = string.Join(' ', scopes),
|
||||
IssuedAt = token.IssuedAt,
|
||||
ExpiresAt = token.ExpiresAt,
|
||||
RevokedAt = token.RevokedAt,
|
||||
RevokedBy = token.RevokedBy,
|
||||
LastUsedAt = token.LastUsedAt,
|
||||
IsActive = !token.RevokedAt.HasValue && token.ExpiresAt > DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user