< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.Controllers.ApplicationTokenController
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Controllers/ApplicationTokenController.cs
Line coverage
100%
Covered lines: 14
Uncovered lines: 0
Coverable lines: 14
Total lines: 78
Line coverage: 100%
Branch coverage
100%
Covered branches: 2
Total branches: 2
Branch coverage: 100%
Method coverage
100%
Covered methods: 4
Fully covered methods: 4
Total methods: 4
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
IssueToken()100%11100%
Self()100%11100%
BearerToken(...)100%22100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Controllers/ApplicationTokenController.cs

#LineLine coverage
 1using ArturRios.Heimdall.Command.Input;
 2using ArturRios.Heimdall.Command.Output;
 3using ArturRios.Heimdall.Query.Input;
 4using ArturRios.Heimdall.Query.Output;
 5using ArturRios.Heimdall.Shared.Messages;
 6using ArturRios.Mediator.Command;
 7using ArturRios.Mediator.Query;
 8using ArturRios.Output;
 9using ArturRios.Util.WebApi.AspNetCore;
 10using ArturRios.Util.WebApi.Security.Attributes;
 11using Microsoft.AspNetCore.Mvc;
 12using Microsoft.AspNetCore.RateLimiting;
 13
 14namespace ArturRios.Heimdall.WebApi.Controllers;
 15
 16/// <summary>
 17///     Authentication for applications (UC-56, UC-57, FR-AP-13…FR-AP-16): exchanging a credential
 18///     for an application token, and checking such a token live.
 19/// </summary>
 20/// <remarks>
 21///     Both actions are anonymous to <c>AuthenticationMiddleware</c>. The middleware knows only
 22///     person tokens — and refuses application tokens (TH-35) — so UC-56 is authenticated by its
 23///     secret and UC-57 validates the bearer token itself.
 24/// </remarks>
 25[Route("api/applications")]
 3726public class ApplicationTokenController(CommandMediator commandMediator, QueryMediator queryMediator) : Controller
 27{
 28    /// <summary>
 29    ///     Exchanges an application's secret for a token valid for ten minutes by default (UC-56).
 30    ///     Every refusal is the same 401, so the endpoint reveals nothing about which applications,
 31    ///     scopes or credentials exist. Rate-limited per client address like the person sign-in.
 32    /// </summary>
 33    [HttpPost("token")]
 34    [AllowAnonymous]
 35    [EnableRateLimiting(Startup.AuthEndpointRateLimitPolicy)]
 36    public async Task<ActionResult<DataOutput<IssueApplicationTokenCommandOutput?>>> IssueToken(
 37        [FromBody] IssueApplicationTokenCommand command)
 38    {
 39        // AF-56a — a body that is not JSON, empty, or carries an id that is not a GUID never reaches
 40        // here: MalformedRequestFilter answers it (NFR-10).
 2241        var result = await commandMediator
 2242            .ExecuteCommandAsync<IssueApplicationTokenCommand, IssueApplicationTokenCommandOutput>(command);
 43
 2244        return result.ToActionResult(statusMap: ApplicationTokenMessageMap.StatusCodes);
 2245    }
 46
 47    /// <summary>
 48    ///     Checks an application token live (UC-57), presented as <c>Authorization: Bearer …</c>.
 49    ///     Answers the application, its scope and the credential when the token is valid and all
 50    ///     three are still live; otherwise one 401. Anonymous to the middleware, which accepts only
 51    ///     person tokens: the handler validates the token itself.
 52    /// </summary>
 53    [HttpGet("self")]
 54    [AllowAnonymous]
 55    public async Task<ActionResult<DataOutput<ApplicationTokenCheckOutput?>>> Self()
 56    {
 1257        var query = new CheckApplicationTokenQuery { Token = BearerToken(Request) };
 58
 1259        var result = await queryMediator
 1260            .ExecuteQueryAsync<CheckApplicationTokenQuery, ApplicationTokenCheckOutput>(query);
 61
 1262        return result.ToActionResult(statusMap: ApplicationTokenMessageMap.StatusCodes);
 1263    }
 64
 65    /// <summary>
 66    ///     The token of a <c>Bearer</c> <c>Authorization</c> header, or <c>null</c>. The scheme is
 67    ///     matched case-insensitively (RFC 9110 §11.1); any other scheme, or none, is no token.
 68    /// </summary>
 69    private static string? BearerToken(HttpRequest request)
 70    {
 71        const string scheme = "Bearer ";
 1272        var header = request.Headers.Authorization.ToString();
 73
 1274        return header.StartsWith(scheme, StringComparison.OrdinalIgnoreCase)
 1275            ? header[scheme.Length..].Trim()
 1276            : null;
 77    }
 78}