Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public static ISystemWebAdapterRemoteServerAppBuilder AddAuthentication(this ISy

builder.Services.AddScoped<IHttpModule, RemoteAppAuthenticationModule>();
var options = builder.Services.AddOptions<RemoteAppAuthenticationServerOptions>()
.Validate(options => !string.IsNullOrEmpty(options.AuthenticationEndpointPath), "AuthenticationEndpointPath must be set");
.ValidateDataAnnotations();

if (configure is not null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,72 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Security.Policy;
using System.Web;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.SystemWebAdapters.Authentication;

internal sealed class RemoteAppAuthenticationModule : IHttpModule
internal sealed class RemoteAppAuthenticationModule : RemoteModule
{
private readonly RemoteAppServerOptions _remoteAppOptions;
private readonly RemoteAppAuthenticationServerOptions _authOptions;
private readonly RemoteAppAuthenticationHttpHandler _remoteAppAuthHandler;

public RemoteAppAuthenticationModule(IOptions<RemoteAppServerOptions> remoteAppOptions, IOptions<RemoteAppAuthenticationServerOptions> authOptions)
: base(remoteAppOptions)
{
if (authOptions is null)
{
throw new ArgumentNullException(nameof(authOptions));
}

if (remoteAppOptions is null)
{
throw new ArgumentNullException(nameof(remoteAppOptions));
}

if (string.IsNullOrEmpty(authOptions.Value.AuthenticationEndpointPath))
{
throw new ArgumentOutOfRangeException(nameof(authOptions.Value.AuthenticationEndpointPath), "Options must specify remote authentication path.");
}
Path = authOptions.Value.AuthenticationEndpointPath;

if (string.IsNullOrEmpty(remoteAppOptions.Value.ApiKey))
{
throw new ArgumentOutOfRangeException(nameof(remoteAppOptions.Value.ApiKey), "Options must specify API key.");
}

if (string.IsNullOrEmpty(remoteAppOptions.Value.ApiKeyHeader))
{
throw new ArgumentOutOfRangeException(nameof(remoteAppOptions.Value.ApiKeyHeader), "Options must specify API key header name.");
}
var handler = new RemoteAppAuthenticationHttpHandler();

_authOptions = authOptions.Value;
_remoteAppOptions = remoteAppOptions.Value;
_remoteAppAuthHandler = new RemoteAppAuthenticationHttpHandler();
MapGet(context => handler);
}

public void Init(HttpApplication context)
{
context.PostMapRequestHandler += (s, _) =>
{
var context = ((HttpApplication)s).Context;
if (string.Equals(context.Request.Path, _authOptions.AuthenticationEndpointPath, StringComparison.OrdinalIgnoreCase)
&& context.Request.HttpMethod.Equals("GET", StringComparison.OrdinalIgnoreCase))
{
MapRemoteAuthenticationHandler(new HttpContextWrapper(context));

if (context.Handler is null)
{
context.ApplicationInstance.CompleteRequest();
}
}
};
}
protected override string Path { get; }

public void MapRemoteAuthenticationHandler(HttpContextBase context)
protected override bool Authenticate(HttpContextBase context)
{
var apiKey = context.Request.Headers.Get(_remoteAppOptions.ApiKeyHeader);
var migrationAuthenticateHeader = context.Request.Headers.Get(AuthenticationConstants.MigrationAuthenticateRequestHeaderName);

if (migrationAuthenticateHeader is null)
Expand Down Expand Up @@ -94,25 +54,17 @@ public void MapRemoteAuthenticationHandler(HttpContextBase context)
context.Response.StatusCode = 400;
}

// Clear any existing handler as this request is now completely handled
context.Handler = null;
return false;
}
else if (apiKey is null || !string.Equals(_remoteAppOptions.ApiKey, apiKey, StringComparison.Ordinal))
else if (!HasValidApiKey(context))
{
// Requests to the authentication endpoint must include a valid API key.
// Requests without an API key or with an invalid API key are considered malformed.
context.Response.StatusCode = 400;

// Clear any existing handler as this request is now completely handled
context.Handler = null;
}
else
{
context.Handler = _remoteAppAuthHandler;
return false;
}
}

public void Dispose()
{
return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.DataAnnotations;

namespace Microsoft.AspNetCore.SystemWebAdapters.Authentication;

public class RemoteAppAuthenticationServerOptions
{
[Required]
public string AuthenticationEndpointPath { get; set; } = AuthenticationConstants.DefaultEndpoint;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="6.0.0" />
<PackageReference Include="System.Text.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ public static ISystemWebAdapterBuilder AddRemoteAppServer(this ISystemWebAdapter
throw new ArgumentNullException(nameof(configure));
}

var options = builder.Services.AddOptions<RemoteAppServerOptions>()
.Validate(options => !string.IsNullOrEmpty(options.ApiKey), "ApiKey must be set")
.Validate(options => !string.IsNullOrEmpty(options.ApiKeyHeader), "ApiKeyHeader must be set");
builder.Services.AddOptions<RemoteAppServerOptions>()
.ValidateDataAnnotations();

configure(new Builder(builder.Services));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Web;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.SystemWebAdapters;

internal abstract class RemoteModule : IHttpModule
{
private readonly IOptions<RemoteAppServerOptions> _options;

private Func<HttpContextBase, IHttpHandler?>? _get;
private Func<HttpContextBase, IHttpHandler?>? _put;

protected RemoteModule(IOptions<RemoteAppServerOptions> options)
{
_options = options;
}

protected abstract string Path { get; }

protected void MapGet(Func<HttpContextBase, IHttpHandler?> handler)
=> _get = handler;

protected void MapPut(Func<HttpContextBase, IHttpHandler?> handler)
=> _put = handler;

protected bool HasValidApiKey(HttpContextBase context)
{
var apiKey = context.Request.Headers.Get(_options.Value.ApiKeyHeader);

return string.Equals(_options.Value.ApiKey, apiKey, StringComparison.Ordinal);
}

protected virtual bool Authenticate(HttpContextBase context)
{
if (HasValidApiKey(context))
{
return true;
}

context.Response.StatusCode = 401;
return false;
}

void IHttpModule.Dispose()
{
}

void IHttpModule.Init(HttpApplication context)
{
context.PostMapRequestHandler += (s, _) =>
{
var context = ((HttpApplication)s).Context;

if (!string.Equals(context.Request.Path, Path, StringComparison.Ordinal))
{
return;
}

context.Handler = null;

HandleRequest(new HttpContextWrapper(context));

if (context.Handler is null)
{
context.ApplicationInstance.CompleteRequest();
}
};
}

public void HandleRequest(HttpContextBase context)
{
if (!Authenticate(context))
{
}
else if (string.Equals("GET", context.Request.HttpMethod, StringComparison.OrdinalIgnoreCase) && _get is { } get)
{
context.Handler = get(context);
}
else if (string.Equals("PUT", context.Request.HttpMethod, StringComparison.OrdinalIgnoreCase) && _put is { } put)
{
context.Handler = put(context);
}
else
{
context.Response.StatusCode = 405;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,82 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Web;
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.RemoteSession;

internal sealed class RemoteSessionModule : IHttpModule
internal sealed class RemoteSessionModule : RemoteModule
{
private readonly RemoteAppServerOptions _remoteAppOptions;
private readonly RemoteAppSessionStateServerOptions _sessionOptions;
private readonly ReadOnlySessionHandler _readonlyHandler;
private readonly GetWriteableSessionHandler _writeableHandler;
private readonly StoreSessionStateHandler _saveHandler;

public RemoteSessionModule(IOptions<RemoteAppSessionStateServerOptions> sessionOptions, IOptions<RemoteAppServerOptions> remoteAppOptions, ILockedSessionCache cache, ISessionSerializer serializer)
: base(remoteAppOptions)
{
_sessionOptions = sessionOptions?.Value ?? throw new ArgumentNullException(nameof(sessionOptions));
_remoteAppOptions = remoteAppOptions?.Value ?? throw new ArgumentNullException(nameof(remoteAppOptions));

if (string.IsNullOrEmpty(_remoteAppOptions.ApiKey))
if (sessionOptions is null)
{
throw new ArgumentOutOfRangeException(nameof(_remoteAppOptions.ApiKey), "API key must not be empty.");
throw new ArgumentNullException(nameof(sessionOptions));
}

_readonlyHandler = new ReadOnlySessionHandler(serializer);
_writeableHandler = new GetWriteableSessionHandler(serializer, cache);
_saveHandler = new StoreSessionStateHandler(cache, _sessionOptions.CookieName);
}

public void Init(HttpApplication context)
{
context.PostMapRequestHandler += (s, _) =>
{
var context = ((HttpApplication)s).Context;
var options = sessionOptions.Value;

// Filter out requests that are not the correct path so we don't create a wrapper for every request
if (!string.Equals(context.Request.Path, _sessionOptions.SessionEndpointPath, StringComparison.OrdinalIgnoreCase))
{
return;
}
Path = options.SessionEndpointPath;

MapRemoteSessionHandler(new HttpContextWrapper(context));
var readonlyHandler = new ReadOnlySessionHandler(serializer);
var writeableHandler = new GetWriteableSessionHandler(serializer, cache);
var saveHandler = new StoreSessionStateHandler(cache, options.CookieName);

if (context.Handler is null)
{
context.ApplicationInstance.CompleteRequest();
}
};
}
MapGet(context => GetIsReadonly(context.Request) ? readonlyHandler : writeableHandler);
MapPut(context => saveHandler);

public void MapRemoteSessionHandler(HttpContextBase context)
{
if (!string.Equals(_remoteAppOptions.ApiKey, context.Request.Headers.Get(_remoteAppOptions.ApiKeyHeader), StringComparison.Ordinal))
{
context.Response.StatusCode = 401;
}
else if (context.Request.HttpMethod.Equals("GET", StringComparison.OrdinalIgnoreCase))
{
context.Handler = GetIsReadonly(context.Request) ? _readonlyHandler : _writeableHandler;
}
else if (context.Request.HttpMethod.Equals("PUT", StringComparison.OrdinalIgnoreCase))
{
context.Handler = _saveHandler;
}
else
{
// HTTP methods other than GET (read) or PUT (write) are not accepted
context.Response.StatusCode = 405; // Method not allowed
}
}

public void Dispose()
{
static bool GetIsReadonly(HttpRequestBase request)
=> bool.TryParse(request.Headers.Get(SessionConstants.ReadOnlyHeaderName), out var result) && result;
}

private static bool GetIsReadonly(HttpRequestBase request)
=> bool.TryParse(request.Headers.Get(SessionConstants.ReadOnlyHeaderName), out var result) && result;
protected override string Path { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public void VerifyAuthenticationRequestHandling(string apiKey, string authMigrat
var request = new Mock<HttpRequestBase>();
request.Setup(r => r.Headers).Returns(headers);
request.Setup(r => r.QueryString).Returns(queryStrings);
request.Setup(r => r.HttpMethod).Returns("GET");
request.Setup(r => r.Url).Returns(new Uri("http://localhost:8080"));

var response = new Mock<HttpResponseBase>();
Expand All @@ -70,7 +71,7 @@ public void VerifyAuthenticationRequestHandling(string apiKey, string authMigrat
context.SetupProperty(c => c.Handler);

// Act
module.MapRemoteAuthenticationHandler(context.Object);
module.HandleRequest(context.Object);

// Assert
Assert.Equal(expectedStatusCode, response.Object.StatusCode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,6 @@ public RemoteSessionModuleTests()
_fixture = new Fixture();
}

[InlineData(null)]
[InlineData("")]
[Theory]
public void VeriyApiKeyIsNotNullOrEmpty(string apiKey)
{
// Arrange
var sessionOptions = Options.Create(new RemoteAppSessionStateServerOptions());
var remoteAppOptions = Options.Create(new RemoteAppServerOptions { ApiKey = apiKey });
var sessions = new Mock<ILockedSessionCache>();
var serializer = new Mock<ISessionSerializer>();

// Act/Assert
Assert.Throws<ArgumentOutOfRangeException>(() => new RemoteSessionModule(sessionOptions, remoteAppOptions, sessions.Object, serializer.Object));
}

[InlineData("GET", "true", 401, ApiKey1, ApiKey2, null)]
[InlineData("GET", "true", 0, ApiKey1, ApiKey1, typeof(ReadOnlySessionHandler))]
[InlineData("GET", "false", 0, ApiKey1, ApiKey1, typeof(GetWriteableSessionHandler))]
Expand Down Expand Up @@ -82,7 +67,7 @@ public void VerifyCorrectHandler(string method, string readOnlyHeaderValue, int
context.SetupProperty(c => c.Handler);

// Act
module.MapRemoteSessionHandler(context.Object);
module.HandleRequest(context.Object);

// Assert
Assert.Equal(statusCode, response.Object.StatusCode);
Expand Down