diff --git a/EnvelopeGenerator.API/Controllers/AuthController.cs b/EnvelopeGenerator.API/Controllers/AuthController.cs index 2ea2daec..b20b134b 100644 --- a/EnvelopeGenerator.API/Controllers/AuthController.cs +++ b/EnvelopeGenerator.API/Controllers/AuthController.cs @@ -73,4 +73,17 @@ public partial class AuthController(IOptions authTokenKeyOptions, => role is not null && !User.IsInRole(role) ? Unauthorized() : Ok(); + + /// + /// Checks whether the caller holds a valid per-envelope receiver token for the given envelope key. + /// The request must carry a cookie named AuthTokenSignFLOWReceiver.{envelopeKey}. + /// + /// The unique envelope key extracted from the route. + /// Valid per-envelope token found. + /// Token is missing, expired or invalid. + [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + [Authorize(Policy = AuthPolicy.Receiver)] + [HttpGet("check/envelope/{envelopeKey}")] + public IActionResult CheckEnvelopeReceiver([FromRoute] string envelopeKey) => Ok(); } \ No newline at end of file diff --git a/EnvelopeGenerator.API/Program.cs b/EnvelopeGenerator.API/Program.cs index 78213734..658995e7 100644 --- a/EnvelopeGenerator.API/Program.cs +++ b/EnvelopeGenerator.API/Program.cs @@ -127,6 +127,9 @@ try var authTokenKeys = config.GetOrDefault(); + // Scheme name used for per-envelope receiver JWT authentication. + const string EnvelopeReceiverScheme = "EnvelopeReceiverJwt"; + builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; @@ -161,6 +164,48 @@ try else if (context.Request.Query.TryGetValue(authTokenKeys.QueryString, out var queryStrToken)) context.Token = queryStrToken; } + return Task.CompletedTask; + } + }; + }) + // Per-envelope receiver scheme: reads the JWT from the cookie named + // AuthTokenSignFLOWReceiver.{envelope_key} where envelope_key is the + // last path segment of the request URL. + // This enables simultaneous authentication for multiple envelopes + // within the same browser session. + .AddJwtBearer(EnvelopeReceiverScheme, opt => + { + opt.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) => + { + var clientParams = deferredProvider.GetOptions(); + var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience); + return [publicKey.SecurityKey]; + }, + ValidateIssuer = true, + ValidIssuer = authTokenKeys.Issuer, + ValidateAudience = true, + ValidAudience = authTokenKeys.Audience, + }; + + opt.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries); + + // Derive the envelope key from the last route segment: /{envelope_key} + var envelopeKey = paths?.LastOrDefault(); + + if (envelopeKey is not null) + { + var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey); + if (context.Request.Cookies.TryGetValue(cookieName, out var cookieToken) && cookieToken is not null) + context.Token = cookieToken; + } + return Task.CompletedTask; } }; @@ -183,8 +228,13 @@ try policy.RequireRole(Role.Sender, Role.Receiver.Full)) .AddPolicy(AuthPolicy.Sender, policy => policy.RequireRole(Role.Sender)) + // Per-envelope policy: uses the dedicated EnvelopeReceiverJwt scheme so it + // never conflicts with the default JwtBearer scheme. .AddPolicy(AuthPolicy.Receiver, policy => - policy.RequireRole(Role.Receiver.Full, "receiver")) + policy + .AddAuthenticationSchemes(EnvelopeReceiverScheme) + .RequireAuthenticatedUser() + .RequireRole(Role.Receiver.Full, "receiver")) .AddPolicy(AuthPolicy.ReceiverTFA, policy => policy.RequireRole(Role.Receiver.TFA));