using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.SelfService.Portal.Core.API.Authorization; 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 = ApiPolicies.TokenManage)] [ProducesResponseType(200, Type = typeof(IEnumerable))] 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 = ApiPolicies.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 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 = ApiPolicies.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 = ApiPolicies.TokenAdmin)] [ProducesResponseType(200, Type = typeof(IEnumerable))] 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 = ApiPolicies.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 = ApiPolicies.TokenManage)] [ProducesResponseType(200, Type = typeof(IEnumerable))] 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(token.ScopesJson) ?? Array.Empty(); 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 }; } } }