Skip to content

Commit 2f51e18

Browse files
FrostyApeOneFrostyApeOne
andauthored
WIP (#57)
Co-authored-by: FrostyApeOne <[email protected]>
1 parent 258c3a0 commit 2f51e18

File tree

16 files changed

+261
-32
lines changed

16 files changed

+261
-32
lines changed

src/DfE.ExternalApplications.Api/appsettings.Development.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,16 @@
3636
"DiscoveryEndpoint": "https://test-oidc.signin.education.gov.uk/.well-known/openid-configuration"
3737
},
3838
"TestAuthentication": {
39-
"Enabled": true,
39+
"Enabled": false,
4040
"JwtSigningKey": "secret",
4141
"JwtIssuer": "test-external-applications",
4242
"JwtAudience": "external-applications-api"
4343
},
4444
"Frontend": {
4545
"Origin": "https://dev.apply-transfer-academy.education.gov.uk"
46+
},
47+
"CypressAuthentication": {
48+
"AllowToggle": true,
49+
"Secret": ""
4650
}
4751
}

src/DfE.ExternalApplications.Api/appsettings.Production.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,9 @@
2222
},
2323
"Features": {
2424
"PerformanceLoggingEnabled": false
25+
},
26+
"CypressAuthentication": {
27+
"AllowToggle": false,
28+
"Secret": ""
2529
}
2630
}

src/DfE.ExternalApplications.Api/appsettings.Test.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,17 @@
3636
"DiscoveryEndpoint": "https://pp-oidc.signin.education.gov.uk/.well-known/openid-configuration"
3737
},
3838
"TestAuthentication": {
39-
"Enabled": true,
39+
"Enabled": false,
4040
"JwtSigningKey": "secret",
4141
"JwtIssuer": "test-external-applications",
4242
"JwtAudience": "external-applications-api"
4343
},
4444
"Frontend": {
4545
"Origin": "https://test.apply-transfer-academy.education.gov.uk"
46+
},
47+
"CypressAuthentication": {
48+
"AllowToggle": true,
49+
"Secret": ""
4650
}
4751
}
4852

src/DfE.ExternalApplications.Application/ApplicationServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using GovUK.Dfe.CoreLibs.Utilities.RateLimiting;
1313
using Microsoft.AspNetCore.Http;
1414
using GovUK.Dfe.CoreLibs.Email;
15+
using GovUK.Dfe.CoreLibs.Security.Interfaces;
1516

1617
namespace Microsoft.Extensions.DependencyInjection
1718
{
@@ -40,6 +41,7 @@ public static IServiceCollection AddApplicationDependencyGroup(
4041
}
4142
});
4243
services.AddScoped<IPermissionCheckerService, ClaimBasedPermissionCheckerService>();
44+
services.AddScoped<ICustomRequestChecker, CypressRequestChecker>();
4345
services.AddTransient<IApplicationFactory, ApplicationFactory>();
4446
services.AddTransient<IUserFactory, UserFactory>();
4547
services.AddTransient<ITemplateFactory, TemplateFactory>();

src/DfE.ExternalApplications.Application/DfE.ExternalApplications.Application.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<PackageReference Include="GovUK.Dfe.CoreLibs.Email" Version="0.1.0-prerelease-146" />
2222
<PackageReference Include="GovUK.Dfe.CoreLibs.FileStorage" Version="0.1.3" />
2323
<PackageReference Include="GovUK.Dfe.CoreLibs.Notifications" Version="0.1.5" />
24-
<PackageReference Include="GovUK.Dfe.CoreLibs.Security" Version="1.1.17" />
24+
<PackageReference Include="GovUK.Dfe.CoreLibs.Security" Version="1.1.19-prerelease-7" />
2525
<PackageReference Include="GovUK.Dfe.CoreLibs.Utilities" Version="1.0.16" />
2626
<PackageReference Include="FluentValidation" Version="12.0.0" />
2727
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using GovUK.Dfe.CoreLibs.Security.Interfaces;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.Hosting;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace DfE.ExternalApplications.Application.Services
8+
{
9+
public class CypressRequestChecker(
10+
IHostEnvironment env,
11+
IConfiguration config,
12+
ILogger<CypressRequestChecker> logger)
13+
: ICustomRequestChecker
14+
{
15+
private const string CypressHeaderKey = "X-Cypress-Test";
16+
private const string CypressSecretHeaderKey = "X-Cypress-Secret";
17+
private const string ExpectedCypressValue = "true";
18+
19+
/// <summary>
20+
/// Validates if the current HTTP request is a valid Cypress test request
21+
/// </summary>
22+
/// <param name="httpContext">The HTTP context to validate</param>
23+
/// <returns>True if this is a valid Cypress request with correct headers and secret</returns>
24+
public bool IsValidRequest(HttpContext httpContext)
25+
{
26+
// Check for Cypress header
27+
var cypressHeader = httpContext.Request.Headers[CypressHeaderKey].ToString();
28+
if (!string.Equals(cypressHeader, ExpectedCypressValue, StringComparison.OrdinalIgnoreCase))
29+
{
30+
return false;
31+
}
32+
33+
// Only allow in Development, Staging or Test environments (NOT Production)
34+
if (!(env.IsDevelopment() || env.IsStaging() || env.IsEnvironment("Test")))
35+
{
36+
logger.LogWarning(
37+
"Cypress authentication attempted in {Environment} environment from {IP} - rejected",
38+
env.EnvironmentName,
39+
httpContext.Connection.RemoteIpAddress);
40+
return false;
41+
}
42+
43+
// Check if Cypress toggle is allowed in configuration
44+
var allowCypressToggle = config.GetValue<bool>("CypressAuthentication:AllowToggle");
45+
if (!allowCypressToggle)
46+
{
47+
logger.LogWarning(
48+
"Cypress authentication attempted but AllowToggle is disabled from {IP}",
49+
httpContext.Connection.RemoteIpAddress);
50+
return false;
51+
}
52+
53+
// Validate secret
54+
var expectedSecret = config["CypressAuthentication:Secret"];
55+
var providedSecret = httpContext.Request.Headers[CypressSecretHeaderKey].ToString();
56+
57+
if (string.IsNullOrWhiteSpace(expectedSecret) || string.IsNullOrWhiteSpace(providedSecret))
58+
{
59+
logger.LogWarning(
60+
"Cypress authentication attempted with missing secret from {IP}",
61+
httpContext.Connection.RemoteIpAddress);
62+
return false;
63+
}
64+
65+
var isValid = string.Equals(providedSecret, expectedSecret, StringComparison.Ordinal);
66+
67+
if (isValid)
68+
{
69+
logger.LogInformation(
70+
"Valid Cypress test request detected from {IP} for path {Path}",
71+
httpContext.Connection.RemoteIpAddress,
72+
httpContext.Request.Path);
73+
}
74+
else
75+
{
76+
logger.LogWarning(
77+
"Invalid Cypress secret provided from {IP} for path {Path}",
78+
httpContext.Connection.RemoteIpAddress,
79+
httpContext.Request.Path);
80+
}
81+
82+
return isValid;
83+
}
84+
}
85+
86+
}

src/DfE.ExternalApplications.Application/Users/Commands/RegisterUserCommandHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public async Task<Result<UserDto>> Handle(
3737
{
3838
// Validate external token and extract claims
3939
var externalUser = await externalValidator
40-
.ValidateIdTokenAsync(request.SubjectToken, cancellationToken);
40+
.ValidateIdTokenAsync(request.SubjectToken, false, cancellationToken);
4141

4242
var email = externalUser.FindFirst(ClaimTypes.Email)?.Value
4343
?? throw new SecurityTokenException("RegisterUserCommandHandler > Missing email");

src/DfE.ExternalApplications.Application/Users/Queries/ExchangeTokenQueryHandler.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ public class ExchangeTokenQueryHandler(
1818
IExternalIdentityValidator externalValidator,
1919
IEaRepository<User> userRepo,
2020
IUserTokenService tokenSvc,
21-
IHttpContextAccessor httpCtxAcc)
21+
IHttpContextAccessor httpCtxAcc,
22+
ICustomRequestChecker cypressRequestChecker)
2223
: IRequestHandler<ExchangeTokenQuery, ExchangeTokenDto>
2324
{
2425
public async Task<ExchangeTokenDto> Handle(ExchangeTokenQuery req, CancellationToken ct)
2526
{
27+
var validCypressReq = cypressRequestChecker.IsValidRequest(httpCtxAcc.HttpContext!);
28+
2629
var externalUser = await externalValidator
27-
.ValidateIdTokenAsync(req.SubjectToken, ct);
30+
.ValidateIdTokenAsync(req.SubjectToken, validCypressReq, ct);
2831

2932
var email = externalUser.FindFirst(ClaimTypes.Email)?.Value
3033
?? throw new SecurityTokenException("ExchangeTokenQueryHandler > Missing email");

src/DfE.ExternalApplications.Infrastructure/DfE.ExternalApplications.Infrastructure.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<PackageReference Include="GovUK.Dfe.CoreLibs.Security" Version="1.1.17" />
8+
<PackageReference Include="GovUK.Dfe.CoreLibs.Security" Version="1.1.19-prerelease-7" />
99
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.18" />
1010
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.18" />
1111
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.18">

src/GovUK.Dfe.ExternalApplications.Api.Client/Extensions/ServiceCollectionExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public static IServiceCollection AddExternalApplicationsApiClient<TClientInterfa
3232
services.AddHttpContextAccessor();
3333

3434
// Register handlers
35+
services.AddTransient<HeaderForwardingHandler>();
3536
services.AddTransient<AzureBearerTokenHandler>();
3637

3738
if (apiSettings.RequestTokenExchange)
@@ -95,6 +96,10 @@ public static IServiceCollection AddExternalApplicationsApiClient<TClientInterfa
9596
serviceProvider, httpClient, apiSettings.BaseUrl!);
9697
});
9798

99+
// Add header forwarding handler FIRST in the chain
100+
// This ensures headers like X-Cypress-Test are available for all subsequent handlers
101+
builder.AddHttpMessageHandler<HeaderForwardingHandler>();
102+
98103
if (apiSettings.RequestTokenExchange)
99104
{
100105
// Frontend clients: Use exchange flow

0 commit comments

Comments
 (0)