Skip to content

Commit 581075d

Browse files
committed
Add Sentry logging, fix config/secrets, remove URN restrictions
1 parent 91ae6f3 commit 581075d

21 files changed

Lines changed: 334 additions & 377 deletions

SAPSec.Core/Configuration/PrivateBetaRestrictedAccess.cs

Lines changed: 0 additions & 6 deletions
This file was deleted.

SAPSec.Core/Model/Organisation.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,5 +115,10 @@ public class Organisation
115115
[JsonPropertyName("IsOnAPAR")]
116116
public string? IsOnApar { get; set; }
117117

118-
public List<Service> Services { get; set; } = new();
118+
public List<Service> Services { get; set; } = new();
119+
120+
public bool IsEstablishment => string.Equals(
121+
Category?.Name,
122+
"Establishment",
123+
StringComparison.OrdinalIgnoreCase);
119124
}

SAPSec.Core/Services/UserService.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,5 +141,4 @@ public bool HasRole(ClaimsPrincipal principal, string role)
141141
{
142142
return principal?.IsInRole(role) == true;
143143
}
144-
145144
}

SAPSec.Web/Extensions/DsiAuthenticationExtensions.cs renamed to SAPSec.Web/Authentication/DsiAuthenticationExtensions.cs

Lines changed: 23 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,21 @@
11
using Microsoft.AspNetCore.Authentication;
22
using Microsoft.AspNetCore.Authentication.Cookies;
33
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
4+
using Microsoft.AspNetCore.Authorization;
45
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
56
using Microsoft.IdentityModel.Tokens;
67
using SAPSec.Core.Configuration;
78
using SAPSec.Core.Interfaces.Services;
8-
using SAPSec.Core.Model;
99
using SAPSec.Core.Services;
10+
using SAPSec.Web.Authorization;
11+
using SAPSec.Web.Constants;
1012
using System.Diagnostics.CodeAnalysis;
1113

12-
namespace SAPSec.Web.Extensions;
14+
namespace SAPSec.Web.Authentication;
1315

1416
[ExcludeFromCodeCoverage]
1517
public static class DsiAuthenticationExtensions
1618
{
17-
private static class Routes
18-
{
19-
public const string SignIn = "/Auth/sign-in";
20-
public const string AccessDenied = "/Auth/access-denied";
21-
public const string Home = "/";
22-
public const string SchoolSearch = "/find-a-school";
23-
public const string Error = "/error";
24-
}
25-
2619
private static class CookieSettings
2720
{
2821
public const string Name = "SAPSec.Auth";
@@ -44,7 +37,6 @@ public static IServiceCollection AddDsiAuthentication(
4437
var dsiConfig = GetAndValidateConfiguration(configuration);
4538

4639
services.Configure<DsiConfiguration>(configuration.GetSection("DsiConfiguration"));
47-
services.Configure<PrivateBetaRestrictedAccess>(configuration.GetSection("PrivateBetaRestrictedAccess"));
4840

4941
services
5042
.AddAuthentication(ConfigureAuthenticationDefaults)
@@ -54,6 +46,17 @@ public static IServiceCollection AddDsiAuthentication(
5446

5547
RegisterServices(services);
5648

49+
services.AddAuthorization(o =>
50+
{
51+
const string PolicyName = "DsiAuthorizationPolicy";
52+
o.AddPolicy(PolicyName, policy => policy.AddRequirements(new DsiAuthorizationRequirement()));
53+
var policy = o.GetPolicy(PolicyName);
54+
if (policy is not null)
55+
{
56+
o.DefaultPolicy = policy;
57+
}
58+
});
59+
services.AddScoped<IAuthorizationHandler, DsiAuthorizationHandler>();
5760
return services;
5861
}
5962

@@ -139,177 +142,16 @@ private static OpenIdConnectEvents CreateOpenIdConnectEvents(DsiConfiguration co
139142
{
140143
return new OpenIdConnectEvents
141144
{
142-
OnRedirectToIdentityProvider = context => HandleRedirectToIdentityProvider(context, config),
143-
OnMessageReceived = HandleMessageReceived,
144-
OnAuthorizationCodeReceived = HandleAuthorizationCodeReceived,
145-
OnTokenValidated = HandleTokenValidated,
146-
OnRemoteFailure = HandleRemoteFailure,
147-
OnAuthenticationFailed = HandleAuthenticationFailed,
148-
OnSignedOutCallbackRedirect = HandleSignedOutCallbackRedirect
145+
OnRedirectToIdentityProvider = context => DsiAuthenticationHandler.HandleRedirectToIdentityProvider(context, config),
146+
OnMessageReceived = DsiAuthenticationHandler.HandleMessageReceived,
147+
OnAuthorizationCodeReceived = DsiAuthenticationHandler.HandleAuthorizationCodeReceived,
148+
OnTokenValidated = DsiAuthenticationHandler.HandleTokenValidated,
149+
OnRemoteFailure = DsiAuthenticationHandler.HandleRemoteFailure,
150+
OnAuthenticationFailed = DsiAuthenticationHandler.HandleAuthenticationFailed,
151+
OnSignedOutCallbackRedirect = DsiAuthenticationHandler.HandleSignedOutCallbackRedirect
149152
};
150153
}
151154

152-
private static Task HandleRedirectToIdentityProvider(
153-
RedirectContext context,
154-
DsiConfiguration config)
155-
{
156-
if (IsNonLocalhost(context))
157-
{
158-
SetProductionRedirectUri(context, config);
159-
}
160-
161-
return Task.CompletedTask;
162-
}
163-
164-
private static bool IsNonLocalhost(RedirectContext context)
165-
{
166-
return !context.HttpContext.Request.Host.Host.Contains("localhost");
167-
}
168-
169-
private static void SetProductionRedirectUri(RedirectContext context, DsiConfiguration config)
170-
{
171-
var host = context.HttpContext.Request.Host.ToUriComponent();
172-
context.ProtocolMessage.RedirectUri = $"https://{host}{config.CallbackPath}";
173-
}
174-
175-
private static Task HandleMessageReceived(MessageReceivedContext context)
176-
{
177-
if (IsSpuriousAuthCallbackRequest(context))
178-
{
179-
LogSpuriousRequest(context);
180-
RedirectToHomeAndHandle(context);
181-
}
182-
183-
return Task.CompletedTask;
184-
}
185-
186-
private static bool IsSpuriousAuthCallbackRequest(MessageReceivedContext context)
187-
{
188-
var options = context.Options;
189-
return context.Request.Path == options.CallbackPath &&
190-
context.Request.Method == "GET" &&
191-
!context.Request.Query.ContainsKey("code");
192-
}
193-
194-
private static void LogSpuriousRequest(MessageReceivedContext context)
195-
{
196-
var logger = GetLogger(context.HttpContext);
197-
logger.LogWarning(
198-
"Spurious authentication callback request detected at {Path}",
199-
context.Request.Path);
200-
}
201-
202-
private static void RedirectToHomeAndHandle(MessageReceivedContext context)
203-
{
204-
context.Response.Redirect(Routes.Home);
205-
context.HandleResponse();
206-
}
207-
208-
private static Task HandleAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
209-
{
210-
return Task.CompletedTask;
211-
}
212-
213-
private static async Task HandleTokenValidated(TokenValidatedContext context)
214-
{
215-
var logger = GetLogger(context.HttpContext);
216-
217-
try
218-
{
219-
await ProcessValidatedToken(context);
220-
}
221-
catch (Exception ex)
222-
{
223-
logger.LogError(ex, "Error processing validated token for user");
224-
context.Fail(ex);
225-
}
226-
}
227-
228-
private static async Task ProcessValidatedToken(TokenValidatedContext context)
229-
{
230-
var userService = context.HttpContext.RequestServices
231-
.GetRequiredService<IUserService>();
232-
233-
var user = await userService.GetUserFromClaimsAsync(context.Principal!);
234-
235-
if (user == null)
236-
{
237-
return;
238-
}
239-
240-
await SetOrganisationBasedOnCount(context, userService, user);
241-
}
242-
243-
private static async Task SetOrganisationBasedOnCount(
244-
TokenValidatedContext context,
245-
IUserService userService,
246-
User user)
247-
{
248-
if (user.Organisations.Count == 1)
249-
{
250-
await userService.SetCurrentOrganisationAsync(
251-
context.Principal!,
252-
user.Organisations[0].Id);
253-
}
254-
else if (user.Organisations.Count > 1)
255-
{
256-
context.Properties!.RedirectUri = Routes.SchoolSearch;
257-
}
258-
}
259-
260-
private static Task HandleRemoteFailure(RemoteFailureContext context)
261-
{
262-
var logger = GetLogger(context.HttpContext);
263-
264-
logger.LogError(
265-
context.Failure,
266-
"DSI remote authentication failure: {Message}",
267-
context.Failure?.Message);
268-
269-
RedirectToErrorAndHandle(context);
270-
271-
return Task.CompletedTask;
272-
}
273-
274-
private static Task HandleAuthenticationFailed(AuthenticationFailedContext context)
275-
{
276-
var logger = GetLogger(context.HttpContext);
277-
278-
logger.LogError(
279-
context.Exception,
280-
"DSI authentication failed: {Message}",
281-
context.Exception.Message);
282-
283-
RedirectToErrorAndHandle(context);
284-
285-
return Task.CompletedTask;
286-
}
287-
288-
private static void RedirectToErrorAndHandle(RemoteFailureContext context)
289-
{
290-
context.Response.Redirect(Routes.Error);
291-
context.HandleResponse();
292-
}
293-
294-
private static void RedirectToErrorAndHandle(AuthenticationFailedContext context)
295-
{
296-
context.Response.Redirect(Routes.Error);
297-
context.HandleResponse();
298-
}
299-
300-
private static Task HandleSignedOutCallbackRedirect(RemoteSignOutContext context)
301-
{
302-
context.Response.Redirect(Routes.Home);
303-
context.HandleResponse();
304-
305-
return Task.CompletedTask;
306-
}
307-
308-
private static ILogger<Program> GetLogger(HttpContext httpContext)
309-
{
310-
return httpContext.RequestServices.GetRequiredService<ILogger<Program>>();
311-
}
312-
313155
private static void RegisterServices(IServiceCollection services)
314156
{
315157
services.AddHttpContextAccessor();
@@ -343,4 +185,4 @@ private static DsiConfiguration GetDsiConfigurationForApiClient(IServiceProvider
343185

344186
return dsiConfig;
345187
}
346-
}
188+
}

0 commit comments

Comments
 (0)