Skip to content

Commit

Permalink
Merge pull request #358 from GridProtectionAlliance/simplify-authenti…
Browse files Browse the repository at this point in the history
…cation-handler

GSF-13 Do not use Microsoft.Owin.Security base classes for AuthenticationMiddleware
  • Loading branch information
StephenCWills authored Jan 31, 2025
2 parents a09e390 + 4e6ec58 commit ef34123
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 37 deletions.
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

0 comments on commit ef34123

Please sign in to comment.