Skip to content

Commit dc3ba10

Browse files
committed
fix authentication challenges and response codes
1 parent 30c18aa commit dc3ba10

File tree

4 files changed

+124
-40
lines changed

4 files changed

+124
-40
lines changed

Intersect.Server/Web/ApiService.cs

Lines changed: 80 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@
3535
using Intersect.Server.Web.Controllers;
3636
using Intersect.Server.Web.Controllers.Api;
3737
using Intersect.Server.Web.Controllers.AssetManagement;
38+
using Intersect.Server.Web.Extensions;
3839
using Intersect.Server.Web.Types.Chat;
3940
using Microsoft.AspNetCore.Http.Features;
41+
using Microsoft.Extensions.Primitives;
4042
using MyCSharp.HttpUserAgentParser.AspNetCore.DependencyInjection;
4143
using MyCSharp.HttpUserAgentParser.MemoryCache.DependencyInjection;
4244

@@ -50,12 +52,6 @@ internal partial class ApiService : ApplicationService<ServerContext, IApiServic
5052
private WebApplication? _app;
5153
private static readonly Assembly Assembly = typeof(ApiService).Assembly;
5254

53-
private static readonly string[] ChallengePaths = [
54-
"/api",
55-
"/assets",
56-
"/avatar",
57-
];
58-
5955
private static string GetOptionsName<TOptions>() => typeof(TOptions).Name.Replace("Options", string.Empty);
6056

6157
// ReSharper disable once MemberCanBeMadeStatic.Local
@@ -76,11 +72,13 @@ internal partial class ApiService : ApplicationService<ServerContext, IApiServic
7672
return default;
7773
}
7874

79-
ApplicationContext.Context.Value?.Logger.LogInformation(
75+
ApplicationContext.CurrentContext.Logger.LogInformation(
8076
"Launching Intersect REST API in '{EnvironmentName}' mode...",
8177
builder.Environment.EnvironmentName
8278
);
8379

80+
builder.Services.AddSingleton(ApplicationContext.GetCurrentContext<IApplicationContext>());
81+
8482
var updateServerSection = builder.Configuration.GetSection(GetOptionsName<UpdateServerOptions>());
8583
builder.Services.Configure<UpdateServerOptions>(updateServerSection);
8684

@@ -173,30 +171,16 @@ internal partial class ApiService : ApplicationService<ServerContext, IApiServic
173171

174172
builder.Services.AddSingleton<IntersectAuthenticationManager>();
175173

176-
builder.Services.AddAuthentication(BearerCookieFallbackAuthenticationScheme)
174+
builder.Services.AddAuthentication(
175+
options =>
176+
{
177+
options.DefaultScheme = BearerCookieFallbackAuthenticationScheme;
178+
}
179+
)
177180
.AddCookie(
178181
CookieAuthenticationDefaults.AuthenticationScheme,
179182
options =>
180183
{
181-
// Commenting this out fixes no redirect
182-
// Uncommenting fixed API consumers with expired tokens
183-
// options.ForwardChallenge = JwtBearerDefaults.AuthenticationScheme;
184-
185-
// So the thing that was broken if the above was commented out was the editor (or presumably
186-
// anything that had an expired token) -- I believe the below fixes it
187-
// options.ForwardDefaultSelector = context =>
188-
// {
189-
// var requestPath = context.Request.Path;
190-
// foreach (var challengePath in ChallengePaths)
191-
// {
192-
// if (requestPath.StartsWithSegments(challengePath))
193-
// {
194-
// return JwtBearerDefaults.AuthenticationScheme;
195-
// }
196-
// }
197-
//
198-
// return null;
199-
// };
200184
options.Events.OnSignedIn += async (context) => { };
201185
options.Events.OnSigningIn += async (context) => { };
202186
options.Events.OnSigningOut += async (context) => { };
@@ -221,7 +205,7 @@ internal partial class ApiService : ApplicationService<ServerContext, IApiServic
221205
return;
222206
}
223207

224-
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ApiService>>();
208+
var logger = context.GetAPILogger();
225209
logger.LogInformation(
226210
"Renewing cookie for {UserId}",
227211
updatedPrincipal.FindFirstValue(ClaimTypes.NameIdentifier)
@@ -231,6 +215,34 @@ internal partial class ApiService : ApplicationService<ServerContext, IApiServic
231215
};
232216
options.Events.OnRedirectToLogin += async (context) =>
233217
{
218+
if (context.HttpContext.IsPage())
219+
{
220+
context.GetAPILogger().LogTrace(
221+
"{OnRedirectToLogin} called for page (non-API) endpoint: {Route}",
222+
nameof(CookieAuthenticationEvents.OnRedirectToLogin),
223+
context.HttpContext.Request.Path
224+
);
225+
return;
226+
}
227+
228+
if (context.HttpContext.IsController())
229+
{
230+
context.GetAPILogger().LogTrace(
231+
"{OnRedirectToLogin} called for controller (API) endpoint: {Route}",
232+
nameof(CookieAuthenticationEvents.OnRedirectToLogin),
233+
context.HttpContext.Request.Path
234+
);
235+
236+
context.RedirectUri = string.Empty;
237+
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
238+
context.Response.Headers.Location = StringValues.Empty;
239+
}
240+
241+
context.GetAPILogger().LogWarning(
242+
"{OnRedirectToLogin} called for unclassified endpoint: {Route}",
243+
nameof(CookieAuthenticationEvents.OnRedirectToLogin),
244+
context.HttpContext.Request.Path
245+
);
234246
};
235247
options.Events.OnRedirectToAccessDenied += async (context) =>
236248
{
@@ -256,15 +268,31 @@ internal partial class ApiService : ApplicationService<ServerContext, IApiServic
256268
};
257269
options.ForwardDefaultSelector += context =>
258270
{
259-
var requestPath = context.Request.Path;
260-
foreach (var challengePath in ChallengePaths)
271+
if (context.IsController())
261272
{
262-
if (requestPath.StartsWithSegments(challengePath))
263-
{
264-
return null;
265-
}
273+
return null;
274+
}
275+
276+
if (context.IsPage())
277+
{
278+
return CookieAuthenticationDefaults.AuthenticationScheme;
266279
}
267280

281+
if (context.Request.Headers.Authorization.Count > 0)
282+
{
283+
context.GetAPILogger().LogWarning(
284+
"JwtBearer ForwardDefaultSelector invoked for unclassified endpoint, not forwarding because there is an Authorization header: {Route}",
285+
context.Request.Path
286+
);
287+
288+
return null;
289+
}
290+
291+
context.GetAPILogger().LogWarning(
292+
"JwtBearer ForwardDefaultSelector invoked for unclassified endpoint, falling back to cookie authentication because there is no Authorization header: {Route}",
293+
context.Request.Path
294+
);
295+
268296
return CookieAuthenticationDefaults.AuthenticationScheme;
269297
};
270298
builder.Configuration.Bind($"Api.{nameof(JwtBearerOptions)}", options);
@@ -277,13 +305,12 @@ internal partial class ApiService : ApplicationService<ServerContext, IApiServic
277305
},
278306
OnChallenge = async context =>
279307
{
280-
if (context.AuthenticateFailure != null)
308+
if (context.AuthenticateFailure is not null || context.HttpContext.IsController())
281309
{
282310
// This was needed to make sure authentication failures didn't return 200
283311
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
284312
context.HandleResponse();
285313
}
286-
context.HandleResponse();
287314
},
288315
OnMessageReceived = async context => { },
289316
OnTokenValidated = async context =>
@@ -331,7 +358,7 @@ internal partial class ApiService : ApplicationService<ServerContext, IApiServic
331358

332359
context.Fail("expired_token");
333360

334-
// var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ApiService>>();
361+
// var logger = context.GetAPILogger();
335362
// logger.LogInformation(
336363
// "Changing token for {UserId}",
337364
// updatedPrincipal.FindFirstValue(ClaimTypes.NameIdentifier)
@@ -343,7 +370,8 @@ internal partial class ApiService : ApplicationService<ServerContext, IApiServic
343370
SymmetricSecurityKey issuerKey = new(tokenGenerationOptions.SecretData);
344371
options.TokenValidationParameters.IssuerSigningKey = issuerKey;
345372
}
346-
).AddPolicyScheme(
373+
)
374+
.AddPolicyScheme(
347375
BearerCookieFallbackAuthenticationScheme,
348376
"Bearer-to-Cookie Fallback",
349377
pso =>
@@ -354,10 +382,23 @@ internal partial class ApiService : ApplicationService<ServerContext, IApiServic
354382
{
355383
return JwtBearerDefaults.AuthenticationScheme;
356384
}
357-
else
385+
386+
if (context.IsController())
387+
{
388+
return JwtBearerDefaults.AuthenticationScheme;
389+
}
390+
391+
if (context.IsPage())
358392
{
359393
return CookieAuthenticationDefaults.AuthenticationScheme;
360394
}
395+
396+
context.GetAPILogger().LogWarning(
397+
"Trying to authenticate with no authorization header on unclassified endpoint, falling back to cookie authentication: {Route}",
398+
context.Request.Path
399+
);
400+
401+
return CookieAuthenticationDefaults.AuthenticationScheme;
361402
};
362403
}
363404
);
@@ -583,7 +624,6 @@ internal partial class ApiService : ApplicationService<ServerContext, IApiServic
583624
app.UseResponseCaching();
584625
app.UseOutputCache();
585626

586-
587627
StaticFileOptions staticFileOptions = new()
588628
{
589629
HttpsCompression = HttpsCompressionMode.Compress,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Microsoft.AspNetCore.Authentication;
2+
3+
namespace Intersect.Server.Web.Extensions;
4+
5+
public static class BaseContextExtensions
6+
{
7+
public static ILogger GetAPILogger<TOptions>(this BaseContext<TOptions> context) where TOptions : AuthenticationSchemeOptions
8+
{
9+
return context.HttpContext.GetAPILogger();
10+
}
11+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Microsoft.AspNetCore.Mvc.ApplicationModels;
2+
using Microsoft.AspNetCore.Mvc.Controllers;
3+
4+
namespace Intersect.Server.Web.Extensions;
5+
6+
public static class HttpContextExtensions
7+
{
8+
public static ILogger GetAPILogger(this HttpContext httpContext)
9+
{
10+
return httpContext.RequestServices.GetRequiredService<ILogger<ApiService>>();
11+
}
12+
13+
public static bool IsController(this HttpContext httpContext)
14+
{
15+
if (httpContext.GetEndpoint() is not { } endpoint)
16+
{
17+
return false;
18+
}
19+
20+
return endpoint.Metadata.GetMetadata<ControllerActionDescriptor>() is not null;
21+
}
22+
23+
public static bool IsPage(this HttpContext httpContext)
24+
{
25+
if (httpContext.GetEndpoint() is not { } endpoint)
26+
{
27+
return false;
28+
}
29+
30+
return endpoint.Metadata.GetMetadata<PageRouteMetadata>() is not null;
31+
}
32+
}

Intersect.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=API/@EntryIndexedValue">API</s:String>
23
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FPS/@EntryIndexedValue">FPS</s:String>
34
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GPU/@EntryIndexedValue">GPU</s:String>
45
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IP/@EntryIndexedValue">IP</s:String>

0 commit comments

Comments
 (0)