Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GSF-13 Do not use Microsoft.Owin.Security base classes for AuthenticationMiddleware #358

Merged
merged 1 commit into from
Jan 31, 2025
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
65 changes: 39 additions & 26 deletions Source/Libraries/GSF.Web/Security/AuthenticationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@
using GSF.Reflection;
using GSF.Security;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;

#pragma warning disable SG0015 // Validated - no hard-coded password present

Expand All @@ -51,7 +49,7 @@ namespace GSF.Web.Security
/// <summary>
/// Handles authentication using the configured <see cref="ISecurityProvider"/> implementation in the Owin pipeline.
/// </summary>
public class AuthenticationHandler : AuthenticationHandler<AuthenticationOptions>
public class AuthenticationHandler
{
#region [ Members ]

Expand All @@ -67,11 +65,31 @@ innerException is AggregateException aggEx ?
string.Join("; ", aggEx.Flatten().InnerExceptions.Select(inex => inex.Message)) :
innerException.Message;
}

#endregion


#region [ Constructors ]

/// <summary>
/// Creates a new instance of the <see cref="AuthenticationHandler"/> class.
/// </summary>
/// <param name="context">Context of the request to be authenticated</param>
/// <param name="options">Configuration options for the authentication handler</param>
public AuthenticationHandler(IOwinContext context, AuthenticationOptions options)
{
Request = context.Request;
Response = context.Response;
Options = options;
}

#endregion

#region [ Properties ]

private IOwinRequest Request { get; }
private IOwinResponse Response { get; }
private AuthenticationOptions Options { get; }

// Reads the authorization header value from the request
private AuthenticationHeaderValue AuthorizationHeader
{
Expand Down Expand Up @@ -104,6 +122,8 @@ private IPrincipal AnonymousPrincipal
private string AuthTestPath =>
Options.GetFullAuthTestPath("");

private bool Faulted { get; set; }

private string FaultReason { get; set; }

#endregion
Expand All @@ -112,10 +132,9 @@ private IPrincipal AnonymousPrincipal

/// <summary>
/// The core authentication logic which must be provided by the handler. Will be invoked at most
/// once per request. Do not call directly, call the wrapping Authenticate method instead.
/// once per request.
/// </summary>
/// <returns>The ticket data provided by the authentication logic</returns>
protected override Task<AuthenticationTicket> AuthenticateCoreAsync()
public void Authenticate()
{
try
{
Expand All @@ -125,7 +144,7 @@ protected override Task<AuthenticationTicket> AuthenticateCoreAsync()

// No authentication required for anonymous resources
if (Options.IsAnonymousResource(Request.Path.Value))
return Task.FromResult<AuthenticationTicket>(null);
return;

NameValueCollection queryParameters = System.Web.HttpUtility.ParseQueryString(Request.QueryString.Value);

Expand All @@ -140,8 +159,7 @@ protected override Task<AuthenticationTicket> AuthenticateCoreAsync()
IIdentity logoutIdentity = new GenericIdentity(sessionID.ToString());
string[] logoutRoles = { "logout" };
Request.User = new GenericPrincipal(logoutIdentity, logoutRoles);

return Task.FromResult<AuthenticationTicket>(null);
return;
}

AuthenticationHeaderValue authorization = AuthorizationHeader;
Expand Down Expand Up @@ -214,32 +232,27 @@ Request.User is null ||
else
FaultReason = $"Authentication Pipeline Exception: {ex.Message}";

Log.Publish(MessageLevel.Warning, nameof(AuthenticateCoreAsync), FaultReason, exception: ex);
Log.Publish(MessageLevel.Warning, nameof(Authenticate), FaultReason, exception: ex);
}

return Task.FromResult<AuthenticationTicket>(null);
}

/// <summary>
/// Called once by common code after initialization. If an authentication middle-ware
/// responds directly to specifically known paths it must override this virtual,
/// compare the request path to it's known paths, provide any response information
/// as appropriate, and true to stop further processing.
/// Called once by common code after authentication to respond directly to specifically known paths.
/// </summary>
/// <returns>
/// Returning false will cause the common code to call the next middle-ware in line.
/// Returning true will cause the common code to begin the async completion journey
/// Returning true will cause the common code to call the next middle-ware in line.
/// Returning false will cause the common code to begin the async completion journey
/// without calling the rest of the middle-ware pipeline.
/// </returns>
public override async Task<bool> InvokeAsync()
public async Task<bool> AuthorizeAsync()
{
if (Faulted)
{
// Handle faulted authentication attempts to expose fault reason to client
using TextWriter writer = new StreamWriter(Response.Body, Encoding.UTF8, 4096, true);
await writer.WriteAsync(FaultReason);
Response.StatusCode = (int)HttpStatusCode.InternalServerError;
return !HostingEnvironment.IsHosted;
return HostingEnvironment.IsHosted;
}

// Use Cases:
Expand Down Expand Up @@ -277,20 +290,20 @@ public override async Task<bool> InvokeAsync()
cookieOptions.Path = Options.GetFullAuthTestPath(pathBase);
Response.Cookies.Delete(Options.AuthenticationToken, cookieOptions);

return true; // Abort pipeline
return false; // Abort pipeline
}

// If the user is properly Authenticated but a redirect is requested send that redirect
if (securityPrincipal?.Identity.IsAuthenticated == true && securityPrincipal.Identity.Provider.IsRedirectRequested)
{
Response.Redirect(securityPrincipal.Identity.Provider.RequestedRedirect ?? "/");
return true;
return false; // Abort pipeline
}

// If request is for an anonymous resource or user is properly authenticated, allow
// request to propagate through the Owin pipeline
if (Options.IsAnonymousResource(urlPath) || securityPrincipal?.Identity.IsAuthenticated == true)
return false; // Let pipeline continue
return true; // Let pipeline continue

// Abort pipeline with appropriate response
if (Options.IsAuthFailureRedirectResource(urlPath) && !IsAjaxCall() && !isAuthTest)
Expand Down Expand Up @@ -347,7 +360,7 @@ public override async Task<bool> InvokeAsync()
string failureReason = SecurityPrincipal.GetFailureReasonPhrase(securityPrincipal, AuthorizationHeader?.Scheme, true);
Log.Publish(MessageLevel.Info, "AuthenticationFailure", $"Failed to authenticate {currentIdentity} for {Request.Path}: {failureReason}");

return true; // Abort pipeline
return false; // Abort pipeline
}

private bool UserHasLogoutRole(IPrincipal user)
Expand Down
24 changes: 15 additions & 9 deletions Source/Libraries/GSF.Web/Security/AuthenticationMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,42 @@
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web.Hosting;
using GSF.Diagnostics;
using GSF.Security;
using Microsoft.Owin;
using Microsoft.Owin.Security.Infrastructure;
using Owin;

namespace GSF.Web.Security
{
/// <summary>
/// Middle-ware for configuring authentication using <see cref="ISecurityProvider"/> in the Owin pipeline.
/// </summary>
public class AuthenticationMiddleware : AuthenticationMiddleware<AuthenticationOptions>
public class AuthenticationMiddleware : OwinMiddleware
{
private AuthenticationOptions Options { get; }

/// <summary>
/// Creates a new instance of the <see cref="AuthenticationMiddleware"/> class.
/// </summary>
/// <param name="next">The next middle-ware object in the pipeline.</param>
/// <param name="options">The options for authentication.</param>
public AuthenticationMiddleware(OwinMiddleware next, AuthenticationOptions options)
: base(next, options)
: base(next)
{
Options = options;
}

/// <summary>
/// Returns the authentication handler that provides the authentication logic.
/// </summary>
/// <returns>The authentication handler to provide authentication logic.</returns>
protected override AuthenticationHandler<AuthenticationOptions> CreateHandler() =>
new AuthenticationHandler();
/// <inheritdoc/>
public override async Task Invoke(IOwinContext context)
{
AuthenticationHandler handler = new(context, Options);
handler.Authenticate();

if (await handler.AuthorizeAsync())
await Next.Invoke(context);
}
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions Source/Libraries/GSF.Web/Security/AuthenticationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ namespace GSF.Web.Security
/// <summary>
/// Represents options for authentication using <see cref="AuthenticationHandler"/>.
/// </summary>
public sealed class AuthenticationOptions : Microsoft.Owin.Security.AuthenticationOptions
public sealed class AuthenticationOptions
{
#region [ Members ]

Expand Down Expand Up @@ -109,7 +109,7 @@ public sealed class AuthenticationOptions : Microsoft.Owin.Security.Authenticati
/// <summary>
/// Creates a new instance of the <see cref="AuthenticationOptions"/> class.
/// </summary>
public AuthenticationOptions() : base(SessionHandler.DefaultAuthenticationToken)
public AuthenticationOptions()
{
m_authFailureRedirectResourceCache = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
m_anonymousResourceCache = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
Expand Down