Skip to content

Commit e92ee16

Browse files
authored
Feature/234482 trust leaving (#63)
1 parent 04706c6 commit e92ee16

17 files changed

Lines changed: 1740 additions & 285 deletions

src/DfE.ExternalApplications.Infrastructure/Services/FieldFormattingService.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public string GetFormattedFieldValue(string fieldId, Dictionary<string, object>
4848
{
4949
var fieldValue = GetFieldValue(fieldId, formData);
5050

51+
// DEBUG: Log formatting attempts
52+
53+
5154
if (string.IsNullOrEmpty(fieldValue))
5255
{
5356
return string.Empty;
@@ -58,9 +61,11 @@ public string GetFormattedFieldValue(string fieldId, Dictionary<string, object>
5861
{
5962
if (LooksLikeUploadData(fieldValue))
6063
{
64+
6165
return FormatUploadValue(fieldValue);
6266
}
6367

68+
6469
return FormatAutocompleteValue(fieldValue);
6570
}
6671

@@ -71,6 +76,9 @@ public List<string> GetFormattedFieldValues(string fieldId, Dictionary<string, o
7176
{
7277
var fieldValue = GetFieldValue(fieldId, formData);
7378

79+
// DEBUG: Log formatting attempts for list version
80+
81+
7482
if (string.IsNullOrEmpty(fieldValue))
7583
{
7684
return new List<string>();
@@ -80,9 +88,11 @@ public List<string> GetFormattedFieldValues(string fieldId, Dictionary<string, o
8088
{
8189
if (LooksLikeUploadData(fieldValue))
8290
{
91+
8392
return FormatUploadValuesList(fieldValue);
8493
}
8594

95+
8696
return FormatAutocompleteValuesList(fieldValue);
8797
}
8898

src/DfE.ExternalApplications.Web/Authentication/TestAuthenticationHandler.cs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,25 @@ public TestAuthenticationHandler(
3535
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
3636
{
3737
var requestPath = Context.Request.Path;
38-
Logger.LogDebug(">>>>>>>>>> Authentication >>> TestAuthenticationHandler: HandleAuthenticateAsync called for path {Path}", requestPath);
38+
3939

4040
var email = Context.Session.GetString(SessionKeys.Email);
4141
var token = Context.Session.GetString(SessionKeys.Token);
4242

4343
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(token))
4444
{
45-
Logger.LogDebug(">>>>>>>>>> Authentication >>> TestAuthenticationHandler: No email ({EmailExists}) or token ({TokenExists}) found in session for path {Path}",
46-
!string.IsNullOrEmpty(email), !string.IsNullOrEmpty(token), requestPath);
45+
4746
return Task.FromResult(AuthenticateResult.NoResult());
4847
}
4948

50-
Logger.LogDebug(">>>>>>>>>> Authentication >>> TestAuthenticationHandler: Creating authentication ticket for user {Email} at path {Path}", email, requestPath);
49+
5150

5251
var claims = CreateUserClaims(email);
5352
var identity = new ClaimsIdentity(claims, SchemeName);
5453
var principal = new ClaimsPrincipal(identity);
5554
var ticket = new AuthenticationTicket(principal, CreateAuthenticationProperties(token), SchemeName);
5655

57-
Logger.LogDebug(">>>>>>>>>> Authentication >>> TestAuthenticationHandler: Authentication successful for user {Email} with {ClaimCount} claims",
58-
email, claims.Count());
56+
5957

6058
return Task.FromResult(AuthenticateResult.Success(ticket));
6159
}
@@ -67,8 +65,7 @@ protected override Task HandleChallengeAsync(AuthenticationProperties properties
6765
? "/TestLogin"
6866
: $"/TestLogin?returnUrl={Uri.EscapeDataString(returnUrl)}";
6967

70-
Logger.LogDebug(">>>>>>>>>> Authentication >>> TestAuthenticationHandler: HandleChallengeAsync triggered. Redirecting to {LoginUrl} (return URL: {ReturnUrl})",
71-
loginUrl, returnUrl ?? "None");
68+
7269

7370
Response.Redirect(loginUrl);
7471
return Task.CompletedTask;

src/DfE.ExternalApplications.Web/Filters/ExternalApiExceptionFilter.cs

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,100 @@
44
using Microsoft.AspNetCore.Mvc.RazorPages;
55
using Microsoft.AspNetCore.Mvc;
66
using System.Text.Json;
7+
using DfE.ExternalApplications.Web.Interfaces;
78

89
namespace DfE.ExternalApplications.Web.Filters
910
{
1011
public class ExternalApiPageExceptionFilter : IAsyncPageFilter
1112
{
13+
1214
public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
1315
=> Task.CompletedTask;
1416

1517
public async Task OnPageHandlerExecutionAsync(
1618
PageHandlerExecutingContext context,
1719
PageHandlerExecutionDelegate next)
1820
{
21+
22+
// DETECT UPLOAD REQUESTS BEFORE EXECUTION
23+
var uploadInfo = DetectUploadRequest(context);
24+
if (uploadInfo.isUpload)
25+
{
26+
27+
context.HttpContext.Items["UploadRequestInfo"] = uploadInfo;
28+
}
29+
1930
var executedContext = await next();
2031

32+
33+
2134
if (executedContext.Exception is ExternalApplicationsException<ExceptionResponse> ex
2235
&& !executedContext.ExceptionHandled)
2336
{
37+
38+
2439
var r = ex.Result;
2540
var page = context.HandlerInstance as PageModel
2641
?? throw new InvalidOperationException("Page filter only for Razor Pages");
42+
43+
2744

2845
// 1) Validation: attempt to map structured validation errors into ModelState
2946
if (r.StatusCode is 400 or 422)
3047
{
48+
3149
if (TryAddModelStateErrorsFromContext(page, r))
3250
{
51+
3352
executedContext.Result = new PageResult();
3453
executedContext.ExceptionHandled = true;
54+
3555
return;
3656
}
3757
}
3858

3959
if (r.StatusCode == 400 || r.StatusCode == 409)
4060
{
61+
4162
AddNonFieldError(page, ex.Result.Message);
4263

64+
// SPECIAL HANDLING FOR UPLOAD REQUESTS: Use stored upload info
65+
66+
var storedUploadInfo = context.HttpContext.Items.TryGetValue("UploadRequestInfo", out var storedInfo)
67+
? ((bool isUpload, string fieldId))storedInfo
68+
: (false, string.Empty);
69+
70+
if (storedUploadInfo.Item1)
71+
{
72+
73+
try
74+
{
75+
var formErrorStore = context.HttpContext.RequestServices.GetService<DfE.ExternalApplications.Web.Interfaces.IFormErrorStore>();
76+
if (formErrorStore != null)
77+
{
78+
formErrorStore.Save(storedUploadInfo.Item2, page.ModelState);
79+
80+
// Get the return URL from the request
81+
var returnUrl = context.HttpContext.Request.Form["ReturnUrl"].ToString();
82+
if (!string.IsNullOrEmpty(returnUrl))
83+
{
84+
85+
executedContext.Result = new Microsoft.AspNetCore.Mvc.RedirectResult(returnUrl);
86+
executedContext.ExceptionHandled = true;
87+
return;
88+
}
89+
}
90+
}
91+
catch (Exception)
92+
{
93+
94+
}
95+
}
96+
97+
4398
executedContext.Result = new PageResult();
4499
executedContext.ExceptionHandled = true;
100+
45101
return;
46102
}
47103

@@ -58,8 +114,7 @@ public async Task OnPageHandlerExecutionAsync(
58114
{
59115
var logger = context.HttpContext.RequestServices.GetService<ILogger<ExternalApiPageExceptionFilter>>();
60116
var userId = context.HttpContext.User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "Anonymous";
61-
logger?.LogWarning(">>>>>>>>>> Authentication >>> ExternalApiPageExceptionFilter: 401 Unauthorized error for user {UserId} at {Path}. Redirecting to forbidden page.",
62-
userId, context.HttpContext.Request.Path);
117+
63118

64119
page.TempData["ApiErrorId"] = r.ErrorId;
65120
executedContext.Result = new RedirectToPageResult("/Error/Forbidden");
@@ -79,17 +134,13 @@ public async Task OnPageHandlerExecutionAsync(
79134
r.Message?.Contains("expired", StringComparison.OrdinalIgnoreCase) == true ||
80135
r.Message?.Contains("unauthorized", StringComparison.OrdinalIgnoreCase) == true)
81136
{
82-
logger?.LogWarning(">>>>>>>>>> Authentication >>> ExternalApiPageExceptionFilter: 403 Forbidden with token-related error for user {UserId} at {Path}. " +
83-
"Error message: {ErrorMessage}. User claims: {UserClaims}. Redirecting to logout.",
84-
userId, context.HttpContext.Request.Path, r.Message, userClaims);
137+
85138

86139
executedContext.Result = new RedirectToPageResult("/Logout", new { reason = "token_expired" });
87140
}
88141
else
89142
{
90-
logger?.LogWarning(">>>>>>>>>> Authentication >>> ExternalApiPageExceptionFilter: 403 Forbidden error for user {UserId} at {Path}. " +
91-
"User claims: {UserClaims}. Redirecting to forbidden page.",
92-
userId, context.HttpContext.Request.Path, userClaims);
143+
93144

94145
executedContext.Result = new RedirectToPageResult("/Error/Forbidden");
95146
}
@@ -102,6 +153,7 @@ public async Task OnPageHandlerExecutionAsync(
102153
executedContext.Result = new RedirectToPageResult("/Error/General");
103154
executedContext.ExceptionHandled = true;
104155
}
156+
105157
}
106158

107159
private static void AddNonFieldError(PageModel page, string message)
@@ -159,5 +211,33 @@ private static bool TryAddModelStateErrorsFromContext(PageModel page, ExceptionR
159211

160212
return false;
161213
}
214+
215+
private static (bool isUpload, string fieldId) DetectUploadRequest(PageHandlerExecutingContext context)
216+
{
217+
// Check if this is an upload handler
218+
var handlerName = context.HandlerMethod?.Name;
219+
220+
// Handle both possible handler name formats
221+
var isUploadHandler = handlerName == "OnPostUploadFileAsync" || handlerName == "UploadFile";
222+
if (!isUploadHandler)
223+
{
224+
225+
return (false, string.Empty);
226+
}
227+
228+
229+
230+
// Try to get FieldId from form data
231+
if (context.HttpContext.Request.HasFormContentType)
232+
{
233+
var fieldId = context.HttpContext.Request.Form["FieldId"].ToString();
234+
if (!string.IsNullOrEmpty(fieldId))
235+
{
236+
return (true, fieldId);
237+
}
238+
}
239+
240+
return (false, string.Empty);
241+
}
162242
}
163243
}

src/DfE.ExternalApplications.Web/Filters/ExternalApiMvcExceptionFilter.cs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ public void OnException(ExceptionContext context)
2222

2323
if (r.StatusCode is 401)
2424
{
25-
logger.LogWarning(">>>>>>>>>> Authentication >>> ExternalApiMvcExceptionFilter: 401 Unauthorized error for user {UserId} at {Path}. This may indicate an authentication issue.",
26-
context.HttpContext.User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "Anonymous",
27-
context.HttpContext.Request.Path);
25+
2826

2927
context.Result = new UnauthorizedResult();
3028
context.ExceptionHandled = true;
@@ -35,12 +33,7 @@ public void OnException(ExceptionContext context)
3533
var userId = context.HttpContext.User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "Anonymous";
3634
var userClaims = string.Join(", ", context.HttpContext.User?.Claims?.Select(c => $"{c.Type}:{c.Value}") ?? Array.Empty<string>());
3735

38-
logger.LogWarning(">>>>>>>>>> Authentication >>> ExternalApiMvcExceptionFilter: 403 Forbidden error for user {UserId} at {Path}. " +
39-
"User claims: {UserClaims}. " +
40-
"This may indicate insufficient permissions or an expired/invalid token.",
41-
userId,
42-
context.HttpContext.Request.Path,
43-
userClaims);
36+
4437

4538
context.Result = new ForbidResult();
4639
context.ExceptionHandled = true;

src/DfE.ExternalApplications.Web/Middleware/PermissionsCacheMiddleware.cs

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,42 +22,37 @@ public async Task InvokeAsync(HttpContext context)
2222
var user = context.User;
2323
var requestPath = context.Request.Path;
2424

25-
logger?.LogDebug(">>>>>>>>>> Authentication >>> PermissionsCacheMiddleware: Processing request at path {Path}", requestPath);
25+
2626

2727
if (user.Identity?.IsAuthenticated == true)
2828
{
2929
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
30-
logger?.LogDebug(">>>>>>>>>> Authentication >>> PermissionsCacheMiddleware: User {UserId} is authenticated", userId ?? "Unknown");
30+
3131

3232
if (!string.IsNullOrEmpty(userId))
3333
{
3434
var cacheKey = $"{PermissionsCacheKeyPrefix}{userId}";
3535
if (!cache.TryGetValue(cacheKey, out _))
3636
{
37-
logger?.LogDebug(">>>>>>>>>> Authentication >>> PermissionsCacheMiddleware: No cached permissions found for user {UserId}, fetching from API", userId);
37+
3838

3939
try
4040
{
41-
logger?.LogDebug(">>>>>>>>>> Authentication >>> PermissionsCacheMiddleware: Calling API to get permissions for user {UserId}", userId);
4241
var permissions = await usersClient.GetMyPermissionsAsync();
43-
logger?.LogDebug(">>>>>>>>>> Authentication >>> PermissionsCacheMiddleware: Successfully received {PermissionCount} permissions for user {UserId}",
44-
permissions?.Permissions?.Count() ?? 0, userId);
4542
cache.Set(cacheKey, permissions, TimeSpan.FromMinutes(5));
46-
logger?.LogDebug(">>>>>>>>>> Authentication >>> PermissionsCacheMiddleware: Cached permissions for user {UserId}", userId);
43+
4744
}
4845
catch (HttpRequestException ex) when (ex.Message.Contains("403") || ex.Message.Contains("Forbidden"))
4946
{
50-
logger?.LogWarning(">>>>>>>>>> Authentication >>> PermissionsCacheMiddleware: Received 403/Forbidden from API for user {UserId}: {Error}. Token likely invalid/expired, redirecting to logout.",
51-
userId, ex.Message);
47+
5248

5349
// Token is invalid/expired - force re-authentication
5450
context.Response.Redirect("/Logout?reason=token_expired");
5551
return;
5652
}
5753
catch (HttpRequestException ex) when (ex.Message.Contains("401") || ex.Message.Contains("Unauthorized"))
5854
{
59-
logger?.LogWarning(">>>>>>>>>> Authentication >>> PermissionsCacheMiddleware: Received 401/Unauthorized from API for user {UserId}: {Error}. Authentication failed, redirecting to logout.",
60-
userId, ex.Message);
55+
6156

6257
// Authentication failed - force re-authentication
6358
context.Response.Redirect("/Logout?reason=auth_failed");
@@ -66,33 +61,33 @@ public async Task InvokeAsync(HttpContext context)
6661
catch (Exception ex)
6762
{
6863
// Cache empty auth data on other errors but log the issue
69-
logger?.LogWarning(ex, ">>>>>>>>>> Authentication >>> PermissionsCacheMiddleware: Failed to load user permissions for user {UserId}. Error: {Error}", userId, ex.Message);
64+
7065

7166
var emptyAuthData = new UserAuthorizationDto
7267
{
7368
Permissions = Enumerable.Empty<UserPermissionDto>(),
7469
Roles = Enumerable.Empty<string>()
7570
};
7671
cache.Set(cacheKey, emptyAuthData, TimeSpan.FromMinutes(1));
77-
logger?.LogDebug(">>>>>>>>>> Authentication >>> PermissionsCacheMiddleware: Cached empty permissions for user {UserId} due to error", userId);
72+
7873
}
7974
}
8075
else
8176
{
82-
logger?.LogDebug(">>>>>>>>>> Authentication >>> PermissionsCacheMiddleware: Using cached permissions for user {UserId}", userId);
77+
8378
}
8479
}
8580
else
8681
{
87-
logger?.LogWarning(">>>>>>>>>> Authentication >>> PermissionsCacheMiddleware: User is authenticated but has no NameIdentifier claim");
82+
8883
}
8984
}
9085
else
9186
{
92-
logger?.LogDebug(">>>>>>>>>> Authentication >>> PermissionsCacheMiddleware: User is not authenticated");
87+
9388
}
9489

95-
logger?.LogDebug(">>>>>>>>>> Authentication >>> PermissionsCacheMiddleware: Proceeding to next middleware");
90+
9691
await next(context);
9792
}
9893
}

0 commit comments

Comments
 (0)