< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.Security.PasswordHashSaturationFilter
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Security/PasswordHashSaturationFilter.cs
Line coverage
85%
Covered lines: 6
Uncovered lines: 1
Coverable lines: 7
Total lines: 60
Line coverage: 85.7%
Branch coverage
50%
Covered branches: 1
Total branches: 2
Branch coverage: 50%
Method coverage
100%
Covered methods: 1
Fully covered methods: 0
Total methods: 1
Method coverage: 100%
Full method coverage: 0%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
OnException(...)50%2285.71%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Security/PasswordHashSaturationFilter.cs

#LineLine coverage
 1using ArturRios.Heimdall.Command.Services;
 2using ArturRios.Heimdall.Shared.Messages;
 3using ArturRios.Output;
 4using ArturRios.Util.Http;
 5using Microsoft.AspNetCore.Mvc;
 6using Microsoft.AspNetCore.Mvc.Filters;
 7
 8namespace ArturRios.Heimdall.WebApi.Security;
 9
 10/// <summary>
 11///     Turns a <see cref="PasswordHashGateSaturatedException" /> into <c>503 Service Unavailable</c>
 12///     carrying <see cref="AuthMessages.AuthenticationTemporarilyUnavailable" /> (Threat Model
 13///     TH-03).
 14/// </summary>
 15/// <remarks>
 16///     <para>
 17///         A global exception filter rather than a catch in each handler, because saturation is not a
 18///         property of any one use case: every endpoint that derives a password can meet it — login,
 19///         password reset, person creation, enabling and disabling two-factor authentication — and
 20///         each already returns its own errors through its own message map. Adding the same entry to
 21///         five maps and the same try/catch to eight handlers would state one rule in thirteen
 22///         places.
 23///     </para>
 24///     <para>
 25///         503 rather than 500, and with <c>Retry-After</c>, because nothing is wrong: the request
 26///         was well formed, the caller is not at fault, and the condition clears on its own. That is
 27///         also why it must not be logged as an error — it is the gate working.
 28///     </para>
 29///     <para>
 30///         The response says nothing about the account, so an endpoint that answers this reveals no
 31///         more than a slow one would. It cannot be used to tell an existing address from an absent
 32///         one: the decoy verification AF-11a spends passes through the same gate as a real check,
 33///         so both meet saturation on the same terms.
 34///     </para>
 35/// </remarks>
 36public class PasswordHashSaturationFilter : IExceptionFilter
 37{
 38    /// <summary>
 39    ///     Seconds to advise the caller to wait. Shorter than the gate's own wait, since by the time
 40    ///     this is written the request has already spent that long queuing.
 41    /// </summary>
 42    private const int RetryAfterSeconds = 5;
 43
 44    public void OnException(ExceptionContext context)
 45    {
 246        if (context.Exception is not PasswordHashGateSaturatedException)
 47        {
 048            return;
 49        }
 50
 251        var output = ProcessOutput.New.WithError(AuthMessages.AuthenticationTemporarilyUnavailable);
 52
 253        context.Result = new ObjectResult(output) { StatusCode = HttpStatusCodes.ServiceUnavailable };
 254        context.HttpContext.Response.Headers.RetryAfter = RetryAfterSeconds.ToString();
 55
 56        // Marked handled so it is not also logged as an unhandled fault. A saturated gate under load
 57        // would otherwise fill the log with stack traces describing the protection doing its job.
 258        context.ExceptionHandled = true;
 259    }
 60}