| | | 1 | | using ArturRios.Output; |
| | | 2 | | using ArturRios.Util.Http; |
| | | 3 | | using ArturRios.Util.WebApi.Security.Extensions; |
| | | 4 | | using Microsoft.AspNetCore.Mvc; |
| | | 5 | | using Microsoft.AspNetCore.Mvc.Filters; |
| | | 6 | | |
| | | 7 | | namespace ArturRios.Heimdall.WebApi.Security; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// Global authorization filter enforcing FR-2F-10/NFR-17: a request authenticated with a UC-38 |
| | | 11 | | /// challenge token — <see cref="IdentityUser.MfaPending" /> set — is rejected with |
| | | 12 | | /// <c>401 Unauthorized</c>, with no exceptions. |
| | | 13 | | /// </summary> |
| | | 14 | | /// <remarks> |
| | | 15 | | /// <para> |
| | | 16 | | /// Registered globally (<c>Startup.ConfigureWebApi</c>), so it runs on every controller |
| | | 17 | | /// action, including <c>POST /api/auth/2fa/verify</c> itself — that endpoint never actually |
| | | 18 | | /// trips it, though, because it is <c>[AllowAnonymous]</c> and reads the challenge token as a |
| | | 19 | | /// request body field (<c>VerifyTwoFactorAuthCommand.ChallengeToken</c>), never as a bearer |
| | | 20 | | /// credential, so <c>AuthenticationMiddleware</c> never attaches an <see cref="IdentityUser" /> |
| | | 21 | | /// with <c>MfaPending</c> set for that call in the first place. This filter only ever fires |
| | | 22 | | /// when a challenge token is misused as an <c>Authorization: Bearer</c> header against some |
| | | 23 | | /// other endpoint — exactly the case FR-2F-10 wants blocked. |
| | | 24 | | /// </para> |
| | | 25 | | /// <para> |
| | | 26 | | /// A request that carries no bearer token at all — every anonymous endpoint, and every |
| | | 27 | | /// unauthenticated call to a protected one — resolves <see cref="HttpContextExtensions.GetUser{TUser}" /> |
| | | 28 | | /// to <c>null</c> here, which does not match the <c>MfaPending: true</c> pattern below, so |
| | | 29 | | /// the filter is a no-op for it. It neither blocks nor requires authentication itself; it |
| | | 30 | | /// only ever narrows an identity the pipeline already attached. |
| | | 31 | | /// </para> |
| | | 32 | | /// </remarks> |
| | | 33 | | public class MfaPendingGuardFilter : IAuthorizationFilter |
| | | 34 | | { |
| | | 35 | | public void OnAuthorization(AuthorizationFilterContext context) |
| | | 36 | | { |
| | 1411 | 37 | | var user = context.HttpContext.GetUser<IdentityUser>(); |
| | | 38 | | |
| | 1411 | 39 | | if (user is not { MfaPending: true }) |
| | | 40 | | { |
| | 1377 | 41 | | return; |
| | | 42 | | } |
| | | 43 | | |
| | 34 | 44 | | var output = ProcessOutput.New.WithError( |
| | 34 | 45 | | "A two-factor challenge is still pending completion; this token is not valid here."); |
| | | 46 | | |
| | 34 | 47 | | context.Result = new ObjectResult(output) { StatusCode = HttpStatusCodes.Unauthorized }; |
| | 34 | 48 | | } |
| | | 49 | | } |