From b6ec4dc0f2799b00ead175389fd0f2ab0e140f06 Mon Sep 17 00:00:00 2001 From: Michael Farr Date: Tue, 22 Aug 2017 16:10:07 +1000 Subject: [PATCH 1/8] Update to AspNetCore 2.0.0 Scheme has moved to the RemoteAuthenticationHandler Remove AuthenticationFailed and MessageReceived events. Target netstandard2.0 in the library - with references to net 4.7 libraries. removed SignOut - (its a todo) Add WsFederationPostConfigureOptions (this is almost certainly the wrong way to do this ... I'll fix this soonish). Move ADFS configuration to the JSON configuration file. --- ...NetCore.Authentication.WsFederation.csproj | 13 +- .../Events/AuthenticationFailedContext.cs | 15 - .../Events/BaseWsFederationContext.cs | 8 +- .../Events/IWsFederationEvents.cs | 37 --- .../Events/MessageReceivedContext.cs | 12 - .../Events/RedirectContext.cs | 7 +- .../Events/SecurityTokenContext.cs | 6 +- .../Events/SecurityTokenValidatedContext.cs | 5 +- .../Events/WsFederationEvents.cs | 47 ++- .../WsFederationAppBuilderExtensions.cs | 54 ---- .../WsFederationAuthenticationHandler.cs | 305 +++++++++--------- .../WsFederationAuthenticationMiddleware.cs | 116 ------- .../WsFederationAuthenticationOptions.cs | 11 +- .../WsFederationPostConfigureOptions.cs | 148 +++++++++ Sample/Controllers/AccountController.cs | 4 +- Sample/Sample.csproj | 23 +- Sample/Startup.cs | 29 +- Sample/appsettings.json | 7 + 18 files changed, 379 insertions(+), 468 deletions(-) delete mode 100644 AspNetCore.Authentication.WsFederation/Events/AuthenticationFailedContext.cs delete mode 100644 AspNetCore.Authentication.WsFederation/Events/IWsFederationEvents.cs delete mode 100644 AspNetCore.Authentication.WsFederation/Events/MessageReceivedContext.cs delete mode 100644 AspNetCore.Authentication.WsFederation/WsFederationAppBuilderExtensions.cs delete mode 100644 AspNetCore.Authentication.WsFederation/WsFederationAuthenticationMiddleware.cs create mode 100644 AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs diff --git a/AspNetCore.Authentication.WsFederation/AspNetCore.Authentication.WsFederation.csproj b/AspNetCore.Authentication.WsFederation/AspNetCore.Authentication.WsFederation.csproj index 73322b5..4b868d0 100644 --- a/AspNetCore.Authentication.WsFederation/AspNetCore.Authentication.WsFederation.csproj +++ b/AspNetCore.Authentication.WsFederation/AspNetCore.Authentication.WsFederation.csproj @@ -1,7 +1,7 @@  - net452 + netstandard2.0 chrisdrobison A port of the Katana WsFederation middleware for ASP.NET Core. @@ -14,14 +14,19 @@ - + + - - + + ..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7\System.IdentityModel.dll + + + ..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7\System.Net.Http.WebRequest.dll + \ No newline at end of file diff --git a/AspNetCore.Authentication.WsFederation/Events/AuthenticationFailedContext.cs b/AspNetCore.Authentication.WsFederation/Events/AuthenticationFailedContext.cs deleted file mode 100644 index e9d06cf..0000000 --- a/AspNetCore.Authentication.WsFederation/Events/AuthenticationFailedContext.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using Microsoft.AspNetCore.Http; - -namespace AspNetCore.Authentication.WsFederation -{ - public class AuthenticationFailedContext : BaseWsFederationContext - { - public AuthenticationFailedContext(HttpContext context, WsFederationAuthenticationOptions options) - : base(context, options) - { - } - - public Exception Exception { get; set; } - } -} \ No newline at end of file diff --git a/AspNetCore.Authentication.WsFederation/Events/BaseWsFederationContext.cs b/AspNetCore.Authentication.WsFederation/Events/BaseWsFederationContext.cs index c335cbf..e934a35 100644 --- a/AspNetCore.Authentication.WsFederation/Events/BaseWsFederationContext.cs +++ b/AspNetCore.Authentication.WsFederation/Events/BaseWsFederationContext.cs @@ -5,15 +5,13 @@ namespace AspNetCore.Authentication.WsFederation { - public class BaseWsFederationContext : BaseControlContext + public class BaseWsFederationContext : HandleRequestContext { - public BaseWsFederationContext(HttpContext context, WsFederationAuthenticationOptions options) : base(context) + public BaseWsFederationContext(HttpContext context, WsFederationAuthenticationOptions options, AuthenticationScheme authenticationScheme) : + base(context, authenticationScheme, options) { - Options = options ?? throw new ArgumentNullException(nameof(options)); } - public WsFederationAuthenticationOptions Options { get; } - public WsFederationMessage ProtocolMessage { get; set; } } } \ No newline at end of file diff --git a/AspNetCore.Authentication.WsFederation/Events/IWsFederationEvents.cs b/AspNetCore.Authentication.WsFederation/Events/IWsFederationEvents.cs deleted file mode 100644 index 0358015..0000000 --- a/AspNetCore.Authentication.WsFederation/Events/IWsFederationEvents.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; -using AspNetCore.Authentication.WsFederation.Events; - -namespace AspNetCore.Authentication.WsFederation -{ - /// - /// Specifies events which the invokes to enable developer control over the authentication process. /> - /// - public interface IWsFederationEvents : IRemoteAuthenticationEvents - { - /// - /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. - /// - Task AuthenticationFailed(AuthenticationFailedContext context); - - /// - /// Invoked when a protocol message is first received. - /// - Task MessageReceived(MessageReceivedContext context); - - /// - /// Invoked to manipulate redirects to the identity provider for SignIn, SignOut, or Challenge. - /// - Task RedirectToIdentityProvider(RedirectContext context); - - /// - /// Invoked with the security token that has been extracted from the protocol message. - /// - Task SecurityTokenReceived(SecurityTokenContext context); - - /// - /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. - /// - Task SecurityTokenValidated(SecurityTokenValidatedContext context); - } -} \ No newline at end of file diff --git a/AspNetCore.Authentication.WsFederation/Events/MessageReceivedContext.cs b/AspNetCore.Authentication.WsFederation/Events/MessageReceivedContext.cs deleted file mode 100644 index b26eac2..0000000 --- a/AspNetCore.Authentication.WsFederation/Events/MessageReceivedContext.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace AspNetCore.Authentication.WsFederation.Events -{ - public class MessageReceivedContext : BaseWsFederationContext - { - public MessageReceivedContext(HttpContext context, WsFederationAuthenticationOptions options) - : base(context, options) - { - } - } -} \ No newline at end of file diff --git a/AspNetCore.Authentication.WsFederation/Events/RedirectContext.cs b/AspNetCore.Authentication.WsFederation/Events/RedirectContext.cs index b9f1814..5e7952c 100644 --- a/AspNetCore.Authentication.WsFederation/Events/RedirectContext.cs +++ b/AspNetCore.Authentication.WsFederation/Events/RedirectContext.cs @@ -1,11 +1,12 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using AuthenticationProperties = Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties; namespace AspNetCore.Authentication.WsFederation { public class RedirectContext : BaseWsFederationContext { - public RedirectContext(HttpContext context, WsFederationAuthenticationOptions options) : base(context, options) + public RedirectContext(HttpContext context, WsFederationAuthenticationOptions options, AuthenticationScheme authenticationScheme) : base(context, options, authenticationScheme) { } diff --git a/AspNetCore.Authentication.WsFederation/Events/SecurityTokenContext.cs b/AspNetCore.Authentication.WsFederation/Events/SecurityTokenContext.cs index 39e1cec..b03fa5e 100644 --- a/AspNetCore.Authentication.WsFederation/Events/SecurityTokenContext.cs +++ b/AspNetCore.Authentication.WsFederation/Events/SecurityTokenContext.cs @@ -1,11 +1,11 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; namespace AspNetCore.Authentication.WsFederation.Events { public class SecurityTokenContext : BaseWsFederationContext { - public SecurityTokenContext(HttpContext context, WsFederationAuthenticationOptions options) - : base(context, options) + public SecurityTokenContext(HttpContext context, WsFederationAuthenticationOptions options, AuthenticationScheme authenticationScheme) : base(context, options, authenticationScheme) { } } diff --git a/AspNetCore.Authentication.WsFederation/Events/SecurityTokenValidatedContext.cs b/AspNetCore.Authentication.WsFederation/Events/SecurityTokenValidatedContext.cs index c1aba82..c42f81c 100644 --- a/AspNetCore.Authentication.WsFederation/Events/SecurityTokenValidatedContext.cs +++ b/AspNetCore.Authentication.WsFederation/Events/SecurityTokenValidatedContext.cs @@ -5,9 +5,10 @@ namespace AspNetCore.Authentication.WsFederation.Events { public class SecurityTokenValidatedContext : BaseWsFederationContext { - public SecurityTokenValidatedContext(HttpContext context, WsFederationAuthenticationOptions options) - : base(context, options) + public SecurityTokenValidatedContext(HttpContext context, WsFederationAuthenticationOptions options, AuthenticationScheme authenticationScheme) : base(context, options, authenticationScheme) { } + + public AuthenticationTicket Ticket { get; set; } } } \ No newline at end of file diff --git a/AspNetCore.Authentication.WsFederation/Events/WsFederationEvents.cs b/AspNetCore.Authentication.WsFederation/Events/WsFederationEvents.cs index d19649b..32945bb 100644 --- a/AspNetCore.Authentication.WsFederation/Events/WsFederationEvents.cs +++ b/AspNetCore.Authentication.WsFederation/Events/WsFederationEvents.cs @@ -8,47 +8,46 @@ namespace AspNetCore.Authentication.WsFederation /// /// Specifies events which the invokes to enable developer control over the authentication process. /> /// - public class WsFederationEvents : RemoteAuthenticationEvents, IWsFederationEvents + public class WsFederationEvents : RemoteAuthenticationEvents { + /// - /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. + /// Invoked to manipulate redirects to the identity provider for SignIn, SignOut, or Challenge. /// - public Func OnAuthenticationFailed { get; set; } = - context => TaskCache.CompletedTask; + public Func OnRedirectToIdentityProvider { get; set; } = context => TaskCache.CompletedTask; /// - /// Invoked when a protocol message is first received. + /// Invoked with the security token that has been extracted from the protocol message. + /// + public Func OnSecurityTokenReceived { get; set; } = context => TaskCache.CompletedTask; + + /// + /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. /// - public Func OnMessageReceived { get; set; } = - context => TaskCache.CompletedTask; + public Func OnSecurityTokenValidated { get; set; } = context => TaskCache.CompletedTask; /// /// Invoked to manipulate redirects to the identity provider for SignIn, SignOut, or Challenge. /// - public Func OnRedirectToIdentityProvider { get; set; } = - context => TaskCache.CompletedTask; + public virtual Task RedirectToIdentityProvider(RedirectContext context) + { + return this.OnRedirectToIdentityProvider(context); + } /// /// Invoked with the security token that has been extracted from the protocol message. /// - public Func OnSecurityTokenReceived { get; set; } = - context => TaskCache.CompletedTask; + public virtual Task SecurityTokenReceived(SecurityTokenContext context) + { + return this.OnSecurityTokenReceived(context); + } /// /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. /// - public Func OnSecurityTokenValidated { get; set; } = - context => TaskCache.CompletedTask; - - - public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context); - - public virtual Task MessageReceived(MessageReceivedContext context) => OnMessageReceived(context); - - public Task RedirectToIdentityProvider(RedirectContext context) => OnRedirectToIdentityProvider(context); - - public Task SecurityTokenReceived(SecurityTokenContext context) => OnSecurityTokenReceived(context); - - public Task SecurityTokenValidated(SecurityTokenValidatedContext context) => OnSecurityTokenValidated(context); + public virtual Task SecurityTokenValidated(SecurityTokenValidatedContext context) + { + return this.OnSecurityTokenValidated(context); + } } } \ No newline at end of file diff --git a/AspNetCore.Authentication.WsFederation/WsFederationAppBuilderExtensions.cs b/AspNetCore.Authentication.WsFederation/WsFederationAppBuilderExtensions.cs deleted file mode 100644 index 3035cff..0000000 --- a/AspNetCore.Authentication.WsFederation/WsFederationAppBuilderExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using AspNetCore.Authentication.WsFederation; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Builder -{ - public static class WsFederationAppBuilderExtensions - { - public static IApplicationBuilder UseWsFederationAuthentication(this IApplicationBuilder app, string wtrealm, - string metadataAddress) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (string.IsNullOrEmpty(wtrealm)) - { - throw new ArgumentNullException(nameof(wtrealm)); - } - if (string.IsNullOrEmpty(metadataAddress)) - { - throw new ArgumentNullException(nameof(metadataAddress)); - } - - return - app.UseMiddleware( - Options.Create(new WsFederationAuthenticationOptions - { - Wtrealm = wtrealm, - MetadataAddress = metadataAddress - })); - } - - public static IApplicationBuilder UseWsFederationAuthentication(this IApplicationBuilder app, - WsFederationAuthenticationOptions options) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - if (string.IsNullOrWhiteSpace(options.TokenValidationParameters.ValidAudience)) - { - options.TokenValidationParameters.ValidAudience = options.Wtrealm; - } - - return app.UseMiddleware(Options.Create(options)); - } - } -} \ No newline at end of file diff --git a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs index 512f72c..884f15b 100644 --- a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs +++ b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs @@ -3,41 +3,57 @@ using System.IdentityModel.Tokens; using System.IO; using System.Linq; -using System.Security.Claims; -using System.Text; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; -using System.Xml; using Microsoft.AspNetCore.Authentication; using AspNetCore.Authentication.WsFederation.Events; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Extensions; using Microsoft.IdentityModel.Protocols; namespace AspNetCore.Authentication.WsFederation { - public class WsFederationAuthenticationHandler : RemoteAuthenticationHandler + public class WsFederationAuthenticationHandler : RemoteAuthenticationHandler, Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler { - private readonly ILogger _logger; private WsFederationConfiguration _configuration; - public WsFederationAuthenticationHandler(ILogger logger) + public WsFederationAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger) + : base(options, logger, null, null) { - _logger = logger; + _configuration = options.CurrentValue.Configuration; + } + + /// + /// First the Options.Events value is checked + /// Then a service with Options.EventsType is checked + /// then if neither is non-null, this method is called + /// + protected override Task CreateEventsAsync() + { + return Task.FromResult(new WsFederationEvents()); + } + + + public override Task ShouldHandleRequestAsync() + { + return Task.FromResult(Options.CallbackPath.HasValue && Options.CallbackPath.Equals(Request.PathBase + Request.Path, StringComparison.OrdinalIgnoreCase)); } /// /// Authenticate the user identity with the identity provider. /// The method process the request on the endpoint defined by CallbackPath. /// - protected override async Task HandleRemoteAuthenticateAsync() + protected override async Task HandleRemoteAuthenticateAsync() { // Allow login to be constrained to a specific path. - if (Options.CallbackPath.HasValue && !Options.CallbackPath.Equals(Request.PathBase + Request.Path, StringComparison.OrdinalIgnoreCase)) + if (!await ShouldHandleRequestAsync()) + //if (Options.CallbackPath.HasValue && !Options.CallbackPath.Equals(Request.Path, StringComparison.OrdinalIgnoreCase)) { // Not for us. - return AuthenticateResult.Skip(); + Logger.LogDebug($"Skipping {Options.CallbackPath} != {Request.Path}"); + return HandleRequestResult.SkipHandler(); } WsFederationMessage wsFederationMessage = null; @@ -72,74 +88,66 @@ protected override async Task HandleRemoteAuthenticateAsync( if (Options.SkipUnrecognizedRequests) { // Not for us? - return AuthenticateResult.Skip(); + return HandleRequestResult.SkipHandler(); } - return AuthenticateResult.Fail("No message"); + return HandleRequestResult.Fail("No message"); } try { - var messageReceivedContext = await RunMessageReceivedEventAsync(wsFederationMessage); - AuthenticateResult result; - if (messageReceivedContext.CheckEventResult(out result)) - { - return result; - } - if (wsFederationMessage.Wresult == null) { - return AuthenticateResult.Fail("Received a sign-in message without a WResult."); + return HandleRequestResult.Fail("Received a sign-in message without a WResult."); } var token = wsFederationMessage.GetToken(); if (string.IsNullOrWhiteSpace(token)) { - return AuthenticateResult.Fail("Received a sign-in message without a token."); + return HandleRequestResult.Fail("Received a sign-in message without a token."); } var securityTokenContext = await RunSecurityTokenReceivedEventAsync(wsFederationMessage); - if (securityTokenContext.CheckEventResult(out result)) + if (securityTokenContext.Result?.Handled == true) { - return result; + return HandleRequestResult.Success(securityTokenContext.Result.Ticket); } if (_configuration == null) { - _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(CancellationToken.None); } // Copy and augment to avoid cross request race conditions for updated configurations. var tvp = Options.TokenValidationParameters.Clone(); - IEnumerable issuers = new[] {_configuration.Issuer}; + IEnumerable issuers = new[] { _configuration.Issuer }; tvp.ValidIssuers = tvp.ValidIssuers?.Concat(issuers) ?? issuers; tvp.IssuerSigningKeys = tvp.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? _configuration.SigningKeys; + SecurityToken parsedToken; var principal = Options.SecurityTokenHandlers.ValidateToken(token, tvp, out parsedToken); if (!string.IsNullOrEmpty(Options.BootStrapTokenClaimName) && parsedToken != null) { - ClaimsIdentity identity = principal.Identity as ClaimsIdentity; + var identity = principal.Identity as System.Security.Claims.ClaimsIdentity; if (identity != null) { - StringBuilder sb = new StringBuilder(); - var writer = XmlWriter.Create(new StringWriter(sb), new XmlWriterSettings + var sb = new System.Text.StringBuilder(); + var writer = System.Xml.XmlWriter.Create(new StringWriter(sb), new System.Xml.XmlWriterSettings { OmitXmlDeclaration = true }); Options.SecurityTokenHandlers[parsedToken].WriteToken(writer, parsedToken); writer.Flush(); - identity.AddClaim(new Claim(Options.BootStrapTokenClaimName, Convert.ToBase64String(Encoding.UTF8.GetBytes(sb.ToString())))); + identity.AddClaim(new System.Security.Claims.Claim(Options.BootStrapTokenClaimName, Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(sb.ToString())))); } } - // Retrieve our cached redirect uri var state = wsFederationMessage.Wctx; // WsFed allows for uninitiated logins, state may be missing. var properties = GetPropertiesFromWctx(state); - var ticket = new AuthenticationTicket(principal, properties, - Options.AuthenticationScheme); + var ticket = new AuthenticationTicket(principal, properties, Scheme.Name); if (Options.UseTokenLifetime) { @@ -159,13 +167,15 @@ protected override async Task HandleRemoteAuthenticateAsync( var securityTokenValidatedNotification = await RunSecurityTokenValidatedEventAsync(wsFederationMessage, ticket); - return securityTokenValidatedNotification.CheckEventResult(out result) - ? result - : AuthenticateResult.Success(ticket); + if (securityTokenValidatedNotification.Result != null && securityTokenValidatedNotification.Result.Handled) + { + return HandleRequestResult.Success(securityTokenValidatedNotification.Result.Ticket); + } + return HandleRequestResult.Success(ticket); } catch (Exception exception) { - _logger.LogError("Exception occurred while processing message: ", exception); + Logger.LogError("Exception occurred while processing message: ", exception); // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification. if (Options.RefreshOnIssuerKeyNotFound && @@ -176,9 +186,12 @@ protected override async Task HandleRemoteAuthenticateAsync( var authenticationFailedNotification = await RunAuthenticationFailedEventAsync(wsFederationMessage, exception); - return authenticationFailedNotification.CheckEventResult(out AuthenticateResult result) - ? result - : AuthenticateResult.Fail(exception); + + if (authenticationFailedNotification.Result != null && authenticationFailedNotification.Result.Handled) + { + return HandleRequestResult.Fail(authenticationFailedNotification.Result.Failure); + } + return HandleRequestResult.Fail(exception); } } @@ -187,20 +200,27 @@ protected override async Task HandleRemoteAuthenticateAsync( /// deals an authentication interaction as part of it's request flow. (like adding a response header, or /// changing the 401 result to 302 of a login page or external sign-in location.) /// - /// + /// /// True if no other handlers should be called - protected override async Task HandleUnauthorizedAsync(ChallengeContext context) + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { - if (context == null) + if (properties == null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(properties)); } - Logger.LogTrace($"Entering {nameof(WsFederationAuthenticationHandler)}'s HandleUnauthorizedAsync"); if (_configuration == null) { - _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + var httpClient = new HttpClient() + { + Timeout = Options.BackchannelTimeout, + MaxResponseContentBufferSize = 1024 * 1024 * 10 + }; + // 10 MB + var cm = new ConfigurationManager(Options.MetadataAddress, httpClient); + + _configuration = await cm.GetConfigurationAsync(CancellationToken.None); } var baseUri = @@ -214,7 +234,6 @@ protected override async Task HandleUnauthorizedAsync(ChallengeContext con Request.Path + Request.QueryString; - var properties = new AuthenticationProperties(context.Properties); if (string.IsNullOrEmpty(properties.RedirectUri)) { properties.RedirectUri = currentUri; @@ -235,22 +254,20 @@ protected override async Task HandleUnauthorizedAsync(ChallengeContext con wsFederationMessage.Wreply = Options.Wreply; } - var redirectContext = new RedirectContext(Context, Options) + var redirectContext = new RedirectContext(Context, Options, Scheme) { ProtocolMessage = wsFederationMessage, - Properties = properties + Properties = new Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties(properties.Items) }; - await Options.Events.RedirectToIdentityProvider(redirectContext); - if (redirectContext.HandledResponse) + await Options.WsFedEvents.RedirectToIdentityProvider(redirectContext); + if (redirectContext.Result != null && redirectContext.Result.Handled) { Logger.LogDebug("RedirectContext.HandledResponse"); - return true; } - if (redirectContext.Skipped) + if (redirectContext.Result == null || redirectContext.Result.None) { Logger.LogDebug("RedirectContext.Skipped"); - return false; } var redirectUri = redirectContext.ProtocolMessage.CreateSignInUrl(); @@ -259,73 +276,73 @@ protected override async Task HandleUnauthorizedAsync(ChallengeContext con Logger.LogWarning($"The sign-in redirect URI is malformed: {redirectUri}"); } Response.Redirect(redirectUri); - return true; } + + /// /// Handles signout /// /// /// - protected override async Task HandleSignOutAsync(SignOutContext context) - { - if (context == null) - { - return; - } - - Logger.LogTrace($"Entering {nameof(WsFederationAuthenticationHandler)}'s HandleSignOutAsync"); - - if (_configuration == null && Options.ConfigurationManager != null) - { - _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); - } - - var wsFederationMessage = new WsFederationMessage - { - IssuerAddress = _configuration.TokenEndpoint ?? string.Empty, - Wtrealm = Options.Wtrealm, - Wa = WsFederationActions.SignOut - }; - - var properties = new AuthenticationProperties(context.Properties); - if (!string.IsNullOrEmpty(properties?.RedirectUri)) - { - wsFederationMessage.Wreply = properties.RedirectUri; - } - else if (!string.IsNullOrWhiteSpace(Options.SignOutWreply)) - { - wsFederationMessage.Wreply = Options.SignOutWreply; - } - else if (!string.IsNullOrWhiteSpace(Options.Wreply)) - { - wsFederationMessage.Wreply = Options.Wreply; - } - - var redirectContext = new RedirectContext(Context, Options) - { - ProtocolMessage = wsFederationMessage - }; - await Options.Events.RedirectToIdentityProvider(redirectContext); - if (redirectContext.HandledResponse) - { - Logger.LogDebug("RedirectContext.HandledResponse"); - return; - } - if (redirectContext.Skipped) - { - Logger.LogDebug("RedirectContext.Skipped"); - return; - } - - var redirectUri = redirectContext.ProtocolMessage.CreateSignOutUrl(); - if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) - { - Logger.LogWarning($"The sign-out redirect URI is malformed: {redirectUri}"); - } - Response.Redirect(redirectUri); - } - + //protected override async Task HandleSignOutAsync(SignOutContext context) + //{ + // if (context == null) + // { + // return; + // } + + // Logger.LogTrace($"Entering {nameof(WsFederationAuthenticationHandler)}'s HandleSignOutAsync"); + + // if (_configuration == null && Options.ConfigurationManager != null) + // { + // _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + // } + + // var wsFederationMessage = new WsFederationMessage + // { + // IssuerAddress = _configuration.TokenEndpoint ?? string.Empty, + // Wtrealm = Options.Wtrealm, + // Wa = WsFederationActions.SignOut + // }; + + // var properties = new AuthenticationProperties(context.Properties); + // if (!string.IsNullOrEmpty(properties?.RedirectUri)) + // { + // wsFederationMessage.Wreply = properties.RedirectUri; + // } + // else if (!string.IsNullOrWhiteSpace(Options.SignOutWreply)) + // { + // wsFederationMessage.Wreply = Options.SignOutWreply; + // } + // else if (!string.IsNullOrWhiteSpace(Options.Wreply)) + // { + // wsFederationMessage.Wreply = Options.Wreply; + // } + + // var redirectContext = new RedirectContext(Context, Options) + // { + // ProtocolMessage = wsFederationMessage + // }; + // await Options.Events.RedirectToIdentityProvider(redirectContext); + // if (redirectContext.HandledResponse) + // { + // Logger.LogDebug("RedirectContext.HandledResponse"); + // return; + // } + // if (redirectContext.Skipped) + // { + // Logger.LogDebug("RedirectContext.Skipped"); + // return; + // } + + // var redirectUri = redirectContext.ProtocolMessage.CreateSignOutUrl(); + // if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + // { + // Logger.LogWarning($"The sign-out redirect URI is malformed: {redirectUri}"); + // } + // Response.Redirect(redirectUri); + //} private AuthenticationProperties GetPropertiesFromWctx(string state) { AuthenticationProperties properties = null; @@ -342,43 +359,22 @@ private AuthenticationProperties GetPropertiesFromWctx(string state) return properties; } - private async Task RunMessageReceivedEventAsync(WsFederationMessage message) - { - Logger.LogTrace($"MessageReceived: {message.BuildRedirectUrl()}"); - var messageReceivedContext = new MessageReceivedContext(Context, Options) - { - ProtocolMessage = message - }; - - await Options.Events.MessageReceived(messageReceivedContext); - if (messageReceivedContext.HandledResponse) - { - Logger.LogDebug("MessageReceivedContext.HandledResponse"); - } - else if (messageReceivedContext.Skipped) - { - Logger.LogDebug("MessageReceivedContext.Skipped"); - } - - return messageReceivedContext; - } - private async Task RunSecurityTokenReceivedEventAsync(WsFederationMessage message) { Logger.LogTrace($"SecurityTokenReceived: {message.GetToken()}"); - var securityTokenContext = new SecurityTokenContext(Context, Options) + var securityTokenContext = new SecurityTokenContext(Context, Options, Scheme) { ProtocolMessage = message }; - await Options.Events.SecurityTokenReceived(securityTokenContext); - if (securityTokenContext.HandledResponse) + await Options.WsFedEvents.SecurityTokenReceived(securityTokenContext); + if (securityTokenContext.Result != null && securityTokenContext.Result.Handled) { Logger.LogDebug("SecurityTokenContext.HandledResponse"); } - else if (securityTokenContext.Skipped) + else if (securityTokenContext.Result == null || securityTokenContext.Result.None) { - Logger.LogDebug("SecurityTokenContext.HandledResponse"); + Logger.LogDebug("SecurityTokenContext.Skipped"); } return securityTokenContext; @@ -389,18 +385,19 @@ private async Task RunSecurityTokenValidatedEvent AuthenticationTicket ticket) { Logger.LogTrace($"SecurityTokenValidated: {ticket.AuthenticationScheme} {ticket.Principal.Identity.Name}"); - var securityTokenValidateContext = new SecurityTokenValidatedContext(Context, Options) + var securityTokenValidateContext = new SecurityTokenValidatedContext(Context, Options, Scheme) { ProtocolMessage = message, Ticket = ticket }; - await Options.Events.SecurityTokenValidated(securityTokenValidateContext); - if (securityTokenValidateContext.HandledResponse) + await Options.WsFedEvents.SecurityTokenValidated(securityTokenValidateContext); + + if (securityTokenValidateContext.Result != null && securityTokenValidateContext.Result.Handled) { Logger.LogDebug("SecurityTokenValidatedContext.HandledResponse"); } - else if (securityTokenValidateContext.Skipped) + else if (securityTokenValidateContext.Result == null || securityTokenValidateContext.Result.None) { Logger.LogDebug("SecurityTokenValidatedContext.Skipped"); } @@ -408,22 +405,18 @@ private async Task RunSecurityTokenValidatedEvent return securityTokenValidateContext; } - private async Task RunAuthenticationFailedEventAsync(WsFederationMessage message, + private async Task RunAuthenticationFailedEventAsync(WsFederationMessage message, Exception exception) { Logger.LogTrace("AuthenticationFailed"); - var authenticationFailedContext = new AuthenticationFailedContext(Context, Options) - { - ProtocolMessage = message, - Exception = exception - }; + var authenticationFailedContext = new RemoteFailureContext(Context, this.Scheme, Options, exception); - await Options.Events.AuthenticationFailed(authenticationFailedContext); - if (authenticationFailedContext.HandledResponse) + await Options.Events.OnRemoteFailure(authenticationFailedContext); + if (authenticationFailedContext.Result != null && authenticationFailedContext.Result.Handled) { Logger.LogDebug("AuthenticationFailedContext.HandledResponse"); } - else if (authenticationFailedContext.Skipped) + else if (authenticationFailedContext.Result == null || authenticationFailedContext.Result.None) { Logger.LogDebug("AuthenticationFailedContext.Skipped"); } @@ -438,7 +431,7 @@ private string BuildWreply(string targetPath) private static IDictionary> ParseDelimited(string text) { - char[] delimiters = {'&', ';'}; + char[] delimiters = { '&', ';' }; var accumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); var textLength = text.Length; var equalIndex = text.IndexOf('='); @@ -467,7 +460,7 @@ private static IDictionary> ParseDelimited(string text) List existing; if (!accumulator.TryGetValue(name, out existing)) { - accumulator.Add(name, new List(1) {value}); + accumulator.Add(name, new List(1) { value }); } else { diff --git a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationMiddleware.cs b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationMiddleware.cs deleted file mode 100644 index 0c95501..0000000 --- a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationMiddleware.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Diagnostics; -using System.Net.Http; -using System.Net.Security; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Extensions; -using Microsoft.IdentityModel.Protocols; - -namespace AspNetCore.Authentication.WsFederation -{ - public class WsFederationAuthenticationMiddleware : AuthenticationMiddleware - { - public WsFederationAuthenticationMiddleware(RequestDelegate next, - IOptions options, - IOptions sharedOptions, - ILoggerFactory loggerFactory, - IDataProtectionProvider dataProtectionProvider, - UrlEncoder encoder) - : base(next, options, loggerFactory, encoder) - { - if (string.IsNullOrEmpty(Options.SignInScheme)) - { - Options.SignInScheme = sharedOptions.Value.SignInScheme; - } - if (string.IsNullOrEmpty(Options.SignInScheme)) - { - throw new ArgumentException("Options.SignInScheme is required."); - } - - if (string.IsNullOrWhiteSpace(Options.TokenValidationParameters.AuthenticationType)) - { - Options.TokenValidationParameters.AuthenticationType = Options.SignInScheme; - } - - if (Options.StateDataFormat == null) - { - var dataProtector = dataProtectionProvider.CreateProtector( - typeof(WsFederationAuthenticationMiddleware).FullName, - typeof(string).FullName, - Options.AuthenticationScheme, - "v1" - ); - Options.StateDataFormat = new PropertiesDataFormat(dataProtector); - } - - if (Options.SecurityTokenHandlers == null) - { - Options.SecurityTokenHandlers = SecurityTokenHandlerCollectionExtensions.GetDefaultHandlers(); - } - - if (Options.Events == null) - { - Options.Events = new WsFederationEvents(); - } - - Uri wreply; - if (!Options.CallbackPath.HasValue && !string.IsNullOrEmpty(Options.Wreply) && - Uri.TryCreate(Options.Wreply, UriKind.Absolute, out wreply)) - { - Options.CallbackPath = PathString.FromUriComponent(wreply); - } - - if (Options.ConfigurationManager == null) - { - if (Options.Configuration != null) - { - Options.ConfigurationManager = - new StaticConfigurationManager(Options.Configuration); - } - else - { - var httpClient = new HttpClient(ResolveHttpMessageHandler(Options)) - { - Timeout = Options.BackchannelTimeout, - MaxResponseContentBufferSize = 1024 * 1024 * 10 - }; - // 10 MB - Options.ConfigurationManager = - new ConfigurationManager(Options.MetadataAddress, httpClient); - } - } - } - - protected override AuthenticationHandler CreateHandler() - { - return new WsFederationAuthenticationHandler(Logger); - } - - private static HttpMessageHandler ResolveHttpMessageHandler(WsFederationAuthenticationOptions options) - { - var handler = options.BackchannelHttpHandler ?? new WebRequestHandler(); - - // If they provided a validator, apply it or fail. - if (options.BackchannelCertificateValidator != null) - { - // Set the cert validate callback - var webRequestHandler = handler as WebRequestHandler; - if (webRequestHandler == null) - { - throw new InvalidOperationException( - "An BackchannelCertificateValidator cannot be specified at the same " + - "time as an HttpMessageHandler unless it is a WebRequestHandler."); - } - webRequestHandler.ServerCertificateValidationCallback = - new RemoteCertificateValidationCallback(options.BackchannelCertificateValidator); - } - - return handler; - } - } -} \ No newline at end of file diff --git a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationOptions.cs b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationOptions.cs index c38f5d1..18df859 100644 --- a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationOptions.cs +++ b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationOptions.cs @@ -5,8 +5,8 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Tokens; namespace AspNetCore.Authentication.WsFederation { @@ -25,13 +25,10 @@ public WsFederationAuthenticationOptions() /// /// Initializes a new /// - /// corresponds to the IIdentity AuthenticationType property. . + /// corresponds to the IIdentity AuthenticationType property. . public WsFederationAuthenticationOptions(string authenticationScheme) { - AutomaticAuthenticate = true; - AuthenticationScheme = authenticationScheme; CallbackPath = new PathString("/signin-wsfed"); - DisplayName = WsFederationAuthenticationDefaults.Caption; BackchannelTimeout = TimeSpan.FromMinutes(1); UseTokenLifetime = true; RefreshOnIssuerKeyNotFound = true; @@ -82,7 +79,7 @@ public WsFederationAuthenticationOptions(string authenticationScheme) /// /// Gets or sets the to call when processing WsFederation messages. /// - public new IWsFederationEvents Events { get; set; } + public WsFederationEvents WsFedEvents => (WsFederationEvents)Events; /// /// Gets or sets the of s used to read and validate s. @@ -112,7 +109,7 @@ public SecurityTokenHandlerCollection SecurityTokenHandlers /// This is disabled by default. /// public bool SkipUnrecognizedRequests { get; set; } = false; - + /// /// Gets or sets the 'wreply'. /// diff --git a/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs b/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs new file mode 100644 index 0000000..428c7fb --- /dev/null +++ b/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs @@ -0,0 +1,148 @@ +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Net.Security; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Extensions; +using Microsoft.IdentityModel.Protocols; + +namespace AspNetCore.Authentication.WsFederation +{ + public class WsFederationPostConfigureOptions : IOptionsMonitor, IPostConfigureOptions + { + private readonly IDataProtectionProvider _dataProtectionProvider; + + public WsFederationAuthenticationOptions Get(string name) + { + return CurrentValue; + } + + public IDisposable OnChange(Action listener) + { + throw new NotImplementedException(); + } + + public WsFederationAuthenticationOptions CurrentValue { get; private set; } + + public WsFederationPostConfigureOptions( + IOptions options, + IDataProtectionProvider dataProtectionProvider) + { + _dataProtectionProvider = dataProtectionProvider; + ApplyDefaults(options.Value); + } + + private void ApplyDefaults(WsFederationAuthenticationOptions wsFederationAuthenticationOptions) + { + if (string.IsNullOrEmpty(wsFederationAuthenticationOptions.SignInScheme)) + { + throw new ArgumentException("Options.SignInScheme is required."); + } + var nextValue = new WsFederationAuthenticationOptions() + { + SignInScheme = wsFederationAuthenticationOptions.SignInScheme, + TokenValidationParameters = wsFederationAuthenticationOptions.TokenValidationParameters, + Configuration = wsFederationAuthenticationOptions.Configuration, + ClaimsIssuer = wsFederationAuthenticationOptions.ClaimsIssuer, + Backchannel = wsFederationAuthenticationOptions.Backchannel, + BackchannelCertificateValidator = wsFederationAuthenticationOptions.BackchannelCertificateValidator, + BackchannelHttpHandler = wsFederationAuthenticationOptions.BackchannelHttpHandler, + BackchannelTimeout = wsFederationAuthenticationOptions.BackchannelTimeout, + CallbackPath = wsFederationAuthenticationOptions.CallbackPath, + ConfigurationManager = wsFederationAuthenticationOptions.ConfigurationManager, + CorrelationCookie = wsFederationAuthenticationOptions.CorrelationCookie, + DataProtectionProvider = wsFederationAuthenticationOptions.DataProtectionProvider, + Events = wsFederationAuthenticationOptions.Events,//note the framework will call WsFederationAuthenticationHandler.CreateEventsAsync if this and EventsType is null + EventsType = wsFederationAuthenticationOptions.EventsType, + MetadataAddress = wsFederationAuthenticationOptions.MetadataAddress, + RefreshOnIssuerKeyNotFound = wsFederationAuthenticationOptions.RefreshOnIssuerKeyNotFound, + RemoteAuthenticationTimeout = wsFederationAuthenticationOptions.RemoteAuthenticationTimeout, + SaveTokens = wsFederationAuthenticationOptions.SaveTokens, + SecurityTokenHandlers = wsFederationAuthenticationOptions.SecurityTokenHandlers ?? + SecurityTokenHandlerCollectionExtensions.GetDefaultHandlers(), + SignOutWreply = wsFederationAuthenticationOptions.SignOutWreply, + SkipUnrecognizedRequests = wsFederationAuthenticationOptions.SkipUnrecognizedRequests, + StateDataFormat = wsFederationAuthenticationOptions.StateDataFormat ?? new PropertiesDataFormat( + _dataProtectionProvider.CreateProtector( + typeof(WsFederationPostConfigureOptions).FullName, + typeof(string).FullName, + wsFederationAuthenticationOptions.SignInScheme, + "v1" + )), + UseTokenLifetime = wsFederationAuthenticationOptions.UseTokenLifetime, + Wreply = wsFederationAuthenticationOptions.Wreply, + Wtrealm = wsFederationAuthenticationOptions.Wtrealm + }; + + if (string.IsNullOrWhiteSpace(nextValue.TokenValidationParameters.AuthenticationType)) + { + nextValue.TokenValidationParameters.AuthenticationType = nextValue.SignInScheme; + } + if (string.IsNullOrWhiteSpace(nextValue.TokenValidationParameters.ValidAudience)) + { + nextValue.TokenValidationParameters.ValidAudience = nextValue.Wtrealm; + } + + + Uri wreply; + if (!nextValue.CallbackPath.HasValue && !string.IsNullOrEmpty(nextValue.Wreply) && + Uri.TryCreate(nextValue.Wreply, UriKind.Absolute, out wreply)) + { + nextValue.CallbackPath = PathString.FromUriComponent(wreply); + } + + if (nextValue.ConfigurationManager == null) + { + if (nextValue.Configuration != null) + { + nextValue.ConfigurationManager = + new StaticConfigurationManager(nextValue.Configuration); + } + else + { + var httpClient = new HttpClient(ResolveHttpMessageHandler(nextValue)) + { + Timeout = nextValue.BackchannelTimeout, + MaxResponseContentBufferSize = 1024 * 1024 * 10 + }; + // 10 MB + nextValue.ConfigurationManager = + new ConfigurationManager(nextValue.MetadataAddress, httpClient); + } + } + CurrentValue = nextValue; + } + + private static HttpMessageHandler ResolveHttpMessageHandler(WsFederationAuthenticationOptions options) + { + var handler = options.BackchannelHttpHandler ?? new System.Net.Http.WebRequestHandler(); + + // If they provided a validator, apply it or fail. + if (options.BackchannelCertificateValidator != null) + { + // Set the cert validate callback + var webRequestHandler = handler as WebRequestHandler; + if (webRequestHandler == null) + { + throw new InvalidOperationException( + "An BackchannelCertificateValidator cannot be specified at the same " + + "time as an HttpMessageHandler unless it is a WebRequestHandler."); + } + webRequestHandler.ServerCertificateValidationCallback = + new RemoteCertificateValidationCallback(options.BackchannelCertificateValidator); + } + + return handler; + } + + public void PostConfigure(string name, WsFederationAuthenticationOptions options) + { + ApplyDefaults(options); + } + } +} \ No newline at end of file diff --git a/Sample/Controllers/AccountController.cs b/Sample/Controllers/AccountController.cs index 674282b..0d7aab8 100644 --- a/Sample/Controllers/AccountController.cs +++ b/Sample/Controllers/AccountController.cs @@ -18,13 +18,13 @@ public IActionResult Login(string source = "/") return Redirect(source); } - return Challenge(new AuthenticationProperties { RedirectUri = source }, + return Challenge(new Microsoft.AspNetCore.Authentication.AuthenticationProperties { RedirectUri = source }, WsFederationAuthenticationDefaults.AuthenticationType); } public IActionResult Logout() { - return SignOut(new AuthenticationProperties { RedirectUri = "http://localhost:8550/" }, + return SignOut(new Microsoft.AspNetCore.Authentication.AuthenticationProperties { RedirectUri = "http://localhost:8550/" }, CookieAuthenticationDefaults.AuthenticationScheme, WsFederationAuthenticationDefaults.AuthenticationType); } diff --git a/Sample/Sample.csproj b/Sample/Sample.csproj index e47b3c2..57c7b29 100644 --- a/Sample/Sample.csproj +++ b/Sample/Sample.csproj @@ -4,24 +4,19 @@ net47 - - $(PackageTargetFallback);portable-net45+win8+wp8+wpa81; - - - - - - - - - - - + + + + + + + + - + diff --git a/Sample/Startup.cs b/Sample/Startup.cs index a0f6002..e071055 100644 --- a/Sample/Startup.cs +++ b/Sample/Startup.cs @@ -2,9 +2,11 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Sample { @@ -27,8 +29,18 @@ public void ConfigureServices(IServiceCollection services) { // Add framework services. services.AddMvc(); - services.AddAuthentication( - options => options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme); + + services.Configure(Configuration.GetSection("ADFS")); + services.AddSingleton, WsFederationPostConfigureOptions>(); + + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(o => + { + o.LoginPath = new PathString("/Account/Login/"); + o.ReturnUrlParameter = "source"; + }) + .AddRemoteScheme(WsFederationAuthenticationDefaults.AuthenticationType, "Ws-Fed", null); + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -47,18 +59,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF app.UseExceptionHandler("/Home/Error"); } - app.UseCookieAuthentication(new CookieAuthenticationOptions() - { - AutomaticChallenge = true, - ReturnUrlParameter = "source" - }); - - app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions - { - Wtrealm = "https://cdrobisonxactware.onmicrosoft.com/eb81d25f-9da5-4ed9-af4d-709769a91c6a", - MetadataAddress = - "https://login.windows.net/96708752-4aec-4318-beca-c25ff9bffc1f/FederationMetadata/2007-06/FederationMetadata.xml" - }); + app.UseAuthentication(); app.UseStaticFiles(); diff --git a/Sample/appsettings.json b/Sample/appsettings.json index 5fff67b..68a8248 100644 --- a/Sample/appsettings.json +++ b/Sample/appsettings.json @@ -4,5 +4,12 @@ "LogLevel": { "Default": "Warning" } + }, + "ADFS": { + "MetadataAddress": "https://login.windows.net/96708752-4aec-4318-beca-c25ff9bffc1f/FederationMetadata/2007-06/FederationMetadata.xml", + "Wtrealm": "https://cdrobisonxactware.onmicrosoft.com/eb81d25f-9da5-4ed9-af4d-709769a91c6a", + "Wreply": "https://localhost/signin-wsfed", + "SignInScheme": "Cookies", + "CallbackPath": "/signin-wsfed" } } From eb2d5c491a51f3abbe0f6970914c11af06cbf093 Mon Sep 17 00:00:00 2001 From: Michael Farr Date: Wed, 23 Aug 2017 11:10:13 +1000 Subject: [PATCH 2/8] Remove the IOptionsMonitor hack from WsFederationPostConfigureOptions, switch to the IPostConfigureOptions approach. Add UrlEncoder encoder, ISystemClock clock back into the WsFederationAuthenticationHandler constructor (oops) Remove the authenticationScheme parameter from WsFederationAuthenticationOptions --- .../WsFederationAuthenticationHandler.cs | 10 +- .../WsFederationAuthenticationOptions.cs | 9 -- .../WsFederationPostConfigureOptions.cs | 113 +++++++----------- Sample/Startup.cs | 4 +- 4 files changed, 53 insertions(+), 83 deletions(-) diff --git a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs index 884f15b..ab59441 100644 --- a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs +++ b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Net.Http; +using System.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; @@ -19,10 +20,15 @@ public class WsFederationAuthenticationHandler : RemoteAuthenticationHandler options, ILoggerFactory logger) - : base(options, logger, null, null) + public WsFederationAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) { _configuration = options.CurrentValue.Configuration; + if (options.CurrentValue.Configuration == null && options.CurrentValue.ConfigurationManager == null) + { + Logger.LogCritical("Configuration and ConfigurationManager are both null. WsFederationPostConfigureOptions should at least configure ConfigurationManager."); + } + var instance = options.Get(""); } /// diff --git a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationOptions.cs b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationOptions.cs index 18df859..d77caee 100644 --- a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationOptions.cs +++ b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationOptions.cs @@ -18,15 +18,6 @@ public class WsFederationAuthenticationOptions : RemoteAuthenticationOptions /// Initializes a new /// public WsFederationAuthenticationOptions() - : this(WsFederationAuthenticationDefaults.AuthenticationType) - { - } - - /// - /// Initializes a new - /// - /// corresponds to the IIdentity AuthenticationType property. . - public WsFederationAuthenticationOptions(string authenticationScheme) { CallbackPath = new PathString("/signin-wsfed"); BackchannelTimeout = TimeSpan.FromMinutes(1); diff --git a/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs b/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs index 428c7fb..da87c65 100644 --- a/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs +++ b/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs @@ -13,109 +13,87 @@ namespace AspNetCore.Authentication.WsFederation { - public class WsFederationPostConfigureOptions : IOptionsMonitor, IPostConfigureOptions + public class WsFederationPostConfigureOptions : IPostConfigureOptions { private readonly IDataProtectionProvider _dataProtectionProvider; - public WsFederationAuthenticationOptions Get(string name) + public WsFederationPostConfigureOptions(IDataProtectionProvider dataProtectionProvider) { - return CurrentValue; + _dataProtectionProvider = dataProtectionProvider; } - public IDisposable OnChange(Action listener) + private void ApplyDefaults(WsFederationAuthenticationOptions wsFederationAuthenticationOptions) { - throw new NotImplementedException(); + ConfigureOptions(_dataProtectionProvider, wsFederationAuthenticationOptions); } - - public WsFederationAuthenticationOptions CurrentValue { get; private set; } - - public WsFederationPostConfigureOptions( - IOptions options, - IDataProtectionProvider dataProtectionProvider) + public void PostConfigure(string name, WsFederationAuthenticationOptions options) { - _dataProtectionProvider = dataProtectionProvider; - ApplyDefaults(options.Value); + //SignInScheme will be provided by AuthenticationBuilder.EnsureSignInScheme if not provided by something else. + if (!string.IsNullOrEmpty(options.SignInScheme)) + { + ApplyDefaults(options); + } } - private void ApplyDefaults(WsFederationAuthenticationOptions wsFederationAuthenticationOptions) + private static void ConfigureOptions(IDataProtectionProvider dataProtectionProvider, WsFederationAuthenticationOptions wsFederationAuthenticationOptions) { if (string.IsNullOrEmpty(wsFederationAuthenticationOptions.SignInScheme)) { throw new ArgumentException("Options.SignInScheme is required."); } - var nextValue = new WsFederationAuthenticationOptions() - { - SignInScheme = wsFederationAuthenticationOptions.SignInScheme, - TokenValidationParameters = wsFederationAuthenticationOptions.TokenValidationParameters, - Configuration = wsFederationAuthenticationOptions.Configuration, - ClaimsIssuer = wsFederationAuthenticationOptions.ClaimsIssuer, - Backchannel = wsFederationAuthenticationOptions.Backchannel, - BackchannelCertificateValidator = wsFederationAuthenticationOptions.BackchannelCertificateValidator, - BackchannelHttpHandler = wsFederationAuthenticationOptions.BackchannelHttpHandler, - BackchannelTimeout = wsFederationAuthenticationOptions.BackchannelTimeout, - CallbackPath = wsFederationAuthenticationOptions.CallbackPath, - ConfigurationManager = wsFederationAuthenticationOptions.ConfigurationManager, - CorrelationCookie = wsFederationAuthenticationOptions.CorrelationCookie, - DataProtectionProvider = wsFederationAuthenticationOptions.DataProtectionProvider, - Events = wsFederationAuthenticationOptions.Events,//note the framework will call WsFederationAuthenticationHandler.CreateEventsAsync if this and EventsType is null - EventsType = wsFederationAuthenticationOptions.EventsType, - MetadataAddress = wsFederationAuthenticationOptions.MetadataAddress, - RefreshOnIssuerKeyNotFound = wsFederationAuthenticationOptions.RefreshOnIssuerKeyNotFound, - RemoteAuthenticationTimeout = wsFederationAuthenticationOptions.RemoteAuthenticationTimeout, - SaveTokens = wsFederationAuthenticationOptions.SaveTokens, - SecurityTokenHandlers = wsFederationAuthenticationOptions.SecurityTokenHandlers ?? - SecurityTokenHandlerCollectionExtensions.GetDefaultHandlers(), - SignOutWreply = wsFederationAuthenticationOptions.SignOutWreply, - SkipUnrecognizedRequests = wsFederationAuthenticationOptions.SkipUnrecognizedRequests, - StateDataFormat = wsFederationAuthenticationOptions.StateDataFormat ?? new PropertiesDataFormat( - _dataProtectionProvider.CreateProtector( - typeof(WsFederationPostConfigureOptions).FullName, - typeof(string).FullName, - wsFederationAuthenticationOptions.SignInScheme, - "v1" - )), - UseTokenLifetime = wsFederationAuthenticationOptions.UseTokenLifetime, - Wreply = wsFederationAuthenticationOptions.Wreply, - Wtrealm = wsFederationAuthenticationOptions.Wtrealm - }; - if (string.IsNullOrWhiteSpace(nextValue.TokenValidationParameters.AuthenticationType)) + + wsFederationAuthenticationOptions.SecurityTokenHandlers = + wsFederationAuthenticationOptions.SecurityTokenHandlers ?? + SecurityTokenHandlerCollectionExtensions.GetDefaultHandlers(); + wsFederationAuthenticationOptions.StateDataFormat = + wsFederationAuthenticationOptions.StateDataFormat ?? new PropertiesDataFormat( + dataProtectionProvider.CreateProtector( + typeof(WsFederationPostConfigureOptions).FullName, + typeof(string).FullName, + wsFederationAuthenticationOptions.SignInScheme, + "v1" + )); + wsFederationAuthenticationOptions.UseTokenLifetime = wsFederationAuthenticationOptions.UseTokenLifetime; + wsFederationAuthenticationOptions.Wreply = wsFederationAuthenticationOptions.Wreply; + wsFederationAuthenticationOptions.Wtrealm = wsFederationAuthenticationOptions.Wtrealm; + + if (string.IsNullOrWhiteSpace(wsFederationAuthenticationOptions.TokenValidationParameters.AuthenticationType)) { - nextValue.TokenValidationParameters.AuthenticationType = nextValue.SignInScheme; + wsFederationAuthenticationOptions.TokenValidationParameters.AuthenticationType = wsFederationAuthenticationOptions.SignInScheme; } - if (string.IsNullOrWhiteSpace(nextValue.TokenValidationParameters.ValidAudience)) + if (string.IsNullOrWhiteSpace(wsFederationAuthenticationOptions.TokenValidationParameters.ValidAudience)) { - nextValue.TokenValidationParameters.ValidAudience = nextValue.Wtrealm; + wsFederationAuthenticationOptions.TokenValidationParameters.ValidAudience = wsFederationAuthenticationOptions.Wtrealm; } Uri wreply; - if (!nextValue.CallbackPath.HasValue && !string.IsNullOrEmpty(nextValue.Wreply) && - Uri.TryCreate(nextValue.Wreply, UriKind.Absolute, out wreply)) + if (!wsFederationAuthenticationOptions.CallbackPath.HasValue && !string.IsNullOrEmpty(wsFederationAuthenticationOptions.Wreply) && + Uri.TryCreate(wsFederationAuthenticationOptions.Wreply, UriKind.Absolute, out wreply)) { - nextValue.CallbackPath = PathString.FromUriComponent(wreply); + wsFederationAuthenticationOptions.CallbackPath = PathString.FromUriComponent(wreply); } - if (nextValue.ConfigurationManager == null) + if (wsFederationAuthenticationOptions.ConfigurationManager == null) { - if (nextValue.Configuration != null) + if (wsFederationAuthenticationOptions.Configuration != null) { - nextValue.ConfigurationManager = - new StaticConfigurationManager(nextValue.Configuration); + wsFederationAuthenticationOptions.ConfigurationManager = + new StaticConfigurationManager(wsFederationAuthenticationOptions.Configuration); } else { - var httpClient = new HttpClient(ResolveHttpMessageHandler(nextValue)) + var httpClient = new HttpClient(ResolveHttpMessageHandler(wsFederationAuthenticationOptions)) { - Timeout = nextValue.BackchannelTimeout, + Timeout = wsFederationAuthenticationOptions.BackchannelTimeout, MaxResponseContentBufferSize = 1024 * 1024 * 10 }; // 10 MB - nextValue.ConfigurationManager = - new ConfigurationManager(nextValue.MetadataAddress, httpClient); + wsFederationAuthenticationOptions.ConfigurationManager = + new ConfigurationManager(wsFederationAuthenticationOptions.MetadataAddress, httpClient); } } - CurrentValue = nextValue; } private static HttpMessageHandler ResolveHttpMessageHandler(WsFederationAuthenticationOptions options) @@ -139,10 +117,5 @@ private static HttpMessageHandler ResolveHttpMessageHandler(WsFederationAuthenti return handler; } - - public void PostConfigure(string name, WsFederationAuthenticationOptions options) - { - ApplyDefaults(options); - } } } \ No newline at end of file diff --git a/Sample/Startup.cs b/Sample/Startup.cs index e071055..ba62b8e 100644 --- a/Sample/Startup.cs +++ b/Sample/Startup.cs @@ -30,8 +30,8 @@ public void ConfigureServices(IServiceCollection services) // Add framework services. services.AddMvc(); - services.Configure(Configuration.GetSection("ADFS")); - services.AddSingleton, WsFederationPostConfigureOptions>(); + services.Configure(WsFederationAuthenticationDefaults.AuthenticationType, Configuration.GetSection("ADFS")); + services.AddSingleton, WsFederationPostConfigureOptions>(); services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(o => From 58201cfe0fc53ae10e83f328d61dea19bb070914 Mon Sep 17 00:00:00 2001 From: Michael Farr Date: Wed, 23 Aug 2017 11:37:43 +1000 Subject: [PATCH 3/8] Implement SignOut --- .../WsFederationAuthenticationHandler.cs | 124 +++++++++--------- .../WsFederationPostConfigureOptions.cs | 2 + 2 files changed, 64 insertions(+), 62 deletions(-) diff --git a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs index ab59441..52aa8cf 100644 --- a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs +++ b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs @@ -16,7 +16,7 @@ namespace AspNetCore.Authentication.WsFederation { - public class WsFederationAuthenticationHandler : RemoteAuthenticationHandler, Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler + public class WsFederationAuthenticationHandler : RemoteAuthenticationHandler, IAuthenticationSignOutHandler { private WsFederationConfiguration _configuration; @@ -28,7 +28,6 @@ public WsFederationAuthenticationHandler(IOptionsMonitor @@ -55,7 +54,7 @@ protected override async Task HandleRemoteAuthenticateAsync { // Allow login to be constrained to a specific path. if (!await ShouldHandleRequestAsync()) - //if (Options.CallbackPath.HasValue && !Options.CallbackPath.Equals(Request.Path, StringComparison.OrdinalIgnoreCase)) + //if (Options.CallbackPath.HasValue && !Options.CallbackPath.Equals(Request.Path, StringComparison.OrdinalIgnoreCase)) { // Not for us. Logger.LogDebug($"Skipping {Options.CallbackPath} != {Request.Path}"); @@ -289,66 +288,67 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop /// /// Handles signout /// - /// + /// /// - //protected override async Task HandleSignOutAsync(SignOutContext context) - //{ - // if (context == null) - // { - // return; - // } - - // Logger.LogTrace($"Entering {nameof(WsFederationAuthenticationHandler)}'s HandleSignOutAsync"); - - // if (_configuration == null && Options.ConfigurationManager != null) - // { - // _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); - // } - - // var wsFederationMessage = new WsFederationMessage - // { - // IssuerAddress = _configuration.TokenEndpoint ?? string.Empty, - // Wtrealm = Options.Wtrealm, - // Wa = WsFederationActions.SignOut - // }; - - // var properties = new AuthenticationProperties(context.Properties); - // if (!string.IsNullOrEmpty(properties?.RedirectUri)) - // { - // wsFederationMessage.Wreply = properties.RedirectUri; - // } - // else if (!string.IsNullOrWhiteSpace(Options.SignOutWreply)) - // { - // wsFederationMessage.Wreply = Options.SignOutWreply; - // } - // else if (!string.IsNullOrWhiteSpace(Options.Wreply)) - // { - // wsFederationMessage.Wreply = Options.Wreply; - // } - - // var redirectContext = new RedirectContext(Context, Options) - // { - // ProtocolMessage = wsFederationMessage - // }; - // await Options.Events.RedirectToIdentityProvider(redirectContext); - // if (redirectContext.HandledResponse) - // { - // Logger.LogDebug("RedirectContext.HandledResponse"); - // return; - // } - // if (redirectContext.Skipped) - // { - // Logger.LogDebug("RedirectContext.Skipped"); - // return; - // } - - // var redirectUri = redirectContext.ProtocolMessage.CreateSignOutUrl(); - // if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) - // { - // Logger.LogWarning($"The sign-out redirect URI is malformed: {redirectUri}"); - // } - // Response.Redirect(redirectUri); - //} + public async Task SignOutAsync(AuthenticationProperties properties) + { + + if (properties == null) + { + return; + } + + Logger.LogTrace($"Entering {nameof(WsFederationAuthenticationHandler)}'s HandleSignOutAsync"); + + if (_configuration == null && Options.ConfigurationManager != null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + var wsFederationMessage = new WsFederationMessage + { + IssuerAddress = _configuration.TokenEndpoint ?? string.Empty, + Wtrealm = Options.Wtrealm, + Wa = WsFederationActions.SignOut + }; + + if (!string.IsNullOrEmpty(properties?.RedirectUri)) + { + wsFederationMessage.Wreply = properties.RedirectUri; + } + else if (!string.IsNullOrWhiteSpace(Options.SignOutWreply)) + { + wsFederationMessage.Wreply = Options.SignOutWreply; + } + else if (!string.IsNullOrWhiteSpace(Options.Wreply)) + { + wsFederationMessage.Wreply = Options.Wreply; + } + + var redirectContext = new RedirectContext(Context, Options, Scheme) + { + ProtocolMessage = wsFederationMessage + }; + await Options.WsFedEvents.RedirectToIdentityProvider(redirectContext); + if (redirectContext.Result != null && redirectContext.Result.Handled) + { + Logger.LogDebug("RedirectContext.HandledResponse"); + return; + } + if (redirectContext.Result != null && redirectContext.Result.Skipped) + { + Logger.LogDebug("RedirectContext.Skipped"); + return; + } + + var redirectUri = redirectContext.ProtocolMessage.CreateSignOutUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + Logger.LogWarning($"The sign-out redirect URI is malformed: {redirectUri}"); + } + Response.Redirect(redirectUri); + } + private AuthenticationProperties GetPropertiesFromWctx(string state) { AuthenticationProperties properties = null; diff --git a/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs b/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs index da87c65..c596fc3 100644 --- a/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs +++ b/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs @@ -29,6 +29,8 @@ private void ApplyDefaults(WsFederationAuthenticationOptions wsFederationAuthent public void PostConfigure(string name, WsFederationAuthenticationOptions options) { //SignInScheme will be provided by AuthenticationBuilder.EnsureSignInScheme if not provided by something else. + //However, for reasons that I can't understand, this method always gets called with an uninitialised options and an empty name value + //I'm guessing that is just a bug somewhere in the DefaultAuthorizationPolicyProvider when it attempts to fetch the default configuration. if (!string.IsNullOrEmpty(options.SignInScheme)) { ApplyDefaults(options); From dc5c1c4ecbf160b08c9d28fabadbe2ffe6ae24b4 Mon Sep 17 00:00:00 2001 From: Michael Farr Date: Wed, 23 Aug 2017 12:14:39 +1000 Subject: [PATCH 4/8] Remove unnecessary reference to Tokens.Jwt --- .../AspNetCore.Authentication.WsFederation.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/AspNetCore.Authentication.WsFederation/AspNetCore.Authentication.WsFederation.csproj b/AspNetCore.Authentication.WsFederation/AspNetCore.Authentication.WsFederation.csproj index 4b868d0..ef04510 100644 --- a/AspNetCore.Authentication.WsFederation/AspNetCore.Authentication.WsFederation.csproj +++ b/AspNetCore.Authentication.WsFederation/AspNetCore.Authentication.WsFederation.csproj @@ -16,7 +16,6 @@ - From 247e72e4c3c878ba12a067e6a1fc6f1ee0ab7e63 Mon Sep 17 00:00:00 2001 From: Michael Farr Date: Wed, 23 Aug 2017 15:26:17 +1000 Subject: [PATCH 5/8] Remove PathBase from the callback path comparison. If PathBase is set, the Path will not include the PathBase. --- .../WsFederationAuthenticationHandler.cs | 10 +++------- .../WsFederationPostConfigureOptions.cs | 3 --- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs index 52aa8cf..180a6a0 100644 --- a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs +++ b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs @@ -43,7 +43,7 @@ protected override Task CreateEventsAsync() public override Task ShouldHandleRequestAsync() { - return Task.FromResult(Options.CallbackPath.HasValue && Options.CallbackPath.Equals(Request.PathBase + Request.Path, StringComparison.OrdinalIgnoreCase)); + return Task.FromResult(Options.CallbackPath.HasValue && Options.CallbackPath.Equals(Request.Path, StringComparison.OrdinalIgnoreCase)); } /// @@ -62,7 +62,7 @@ protected override async Task HandleRemoteAuthenticateAsync } WsFederationMessage wsFederationMessage = null; - + // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small. if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(Request.ContentType) @@ -285,11 +285,7 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop - /// - /// Handles signout - /// - /// - /// + /// public async Task SignOutAsync(AuthenticationProperties properties) { diff --git a/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs b/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs index c596fc3..cfe8d41 100644 --- a/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs +++ b/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs @@ -1,12 +1,9 @@ using System; -using System.Diagnostics; using System.Net.Http; using System.Net.Security; -using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Extensions; using Microsoft.IdentityModel.Protocols; From 8e5cd720f6ea5c614c7ddef544ec72d3fa909ffd Mon Sep 17 00:00:00 2001 From: Michael Farr Date: Thu, 24 Aug 2017 09:23:37 +1000 Subject: [PATCH 6/8] Fix relative path for net4.7 build references. --- .../AspNetCore.Authentication.WsFederation.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AspNetCore.Authentication.WsFederation/AspNetCore.Authentication.WsFederation.csproj b/AspNetCore.Authentication.WsFederation/AspNetCore.Authentication.WsFederation.csproj index ef04510..891d524 100644 --- a/AspNetCore.Authentication.WsFederation/AspNetCore.Authentication.WsFederation.csproj +++ b/AspNetCore.Authentication.WsFederation/AspNetCore.Authentication.WsFederation.csproj @@ -21,10 +21,10 @@ - ..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7\System.IdentityModel.dll + C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7\System.IdentityModel.dll - ..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7\System.Net.Http.WebRequest.dll + C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7\System.Net.Http.WebRequest.dll From 9fc43108553eb415bae1e20e9c370688f0b04fa0 Mon Sep 17 00:00:00 2001 From: Michael Farr Date: Fri, 25 Aug 2017 14:23:08 +1000 Subject: [PATCH 7/8] Remove incorrectly cached variable _configuration, fetch it from the manager or static variable every time. --- .../WsFederationAuthenticationHandler.cs | 69 +++++++++---------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs index 180a6a0..684d520 100644 --- a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs +++ b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Net.Http; +using System.Net.Security; using System.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; @@ -18,18 +19,27 @@ namespace AspNetCore.Authentication.WsFederation { public class WsFederationAuthenticationHandler : RemoteAuthenticationHandler, IAuthenticationSignOutHandler { - private WsFederationConfiguration _configuration; - public WsFederationAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { - _configuration = options.CurrentValue.Configuration; - if (options.CurrentValue.Configuration == null && options.CurrentValue.ConfigurationManager == null) + } + + private async Task GetWsFederationConfiguration() + { + if (Options.Configuration == null && Options.ConfigurationManager == null) { Logger.LogCritical("Configuration and ConfigurationManager are both null. WsFederationPostConfigureOptions should at least configure ConfigurationManager."); } + if (Options.ConfigurationManager != null) + { + //in theory ConfigurationManager is caching and refreshing this data appropriately, so we dont need to hold on to an instance of this. + var configuration = await Options.ConfigurationManager.GetConfigurationAsync(CancellationToken.None); + return configuration; + } + return Options.Configuration; } + /// /// First the Options.Events value is checked /// Then a service with Options.EventsType is checked @@ -62,7 +72,7 @@ protected override async Task HandleRemoteAuthenticateAsync } WsFederationMessage wsFederationMessage = null; - + // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small. if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(Request.ContentType) @@ -117,17 +127,19 @@ protected override async Task HandleRemoteAuthenticateAsync return HandleRequestResult.Success(securityTokenContext.Result.Ticket); } - if (_configuration == null) + var configuration = await GetWsFederationConfiguration(); + + if (configuration == null) { - _configuration = await Options.ConfigurationManager.GetConfigurationAsync(CancellationToken.None); + return HandleRequestResult.Fail("Configuration Missing."); } // Copy and augment to avoid cross request race conditions for updated configurations. var tvp = Options.TokenValidationParameters.Clone(); - IEnumerable issuers = new[] { _configuration.Issuer }; + IEnumerable issuers = new[] { configuration.Issuer }; tvp.ValidIssuers = tvp.ValidIssuers?.Concat(issuers) ?? issuers; - tvp.IssuerSigningKeys = tvp.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? - _configuration.SigningKeys; + tvp.IssuerSigningKeys = tvp.IssuerSigningKeys?.Concat(configuration.SigningKeys) ?? + configuration.SigningKeys; SecurityToken parsedToken; @@ -200,13 +212,7 @@ protected override async Task HandleRemoteAuthenticateAsync } } - /// - /// Override this method to deal with 401 challenge concerns, if an authentication scheme in question - /// deals an authentication interaction as part of it's request flow. (like adding a response header, or - /// changing the 401 result to 302 of a login page or external sign-in location.) - /// - /// - /// True if no other handlers should be called + /// protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { if (properties == null) @@ -215,18 +221,6 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop } Logger.LogTrace($"Entering {nameof(WsFederationAuthenticationHandler)}'s HandleUnauthorizedAsync"); - if (_configuration == null) - { - var httpClient = new HttpClient() - { - Timeout = Options.BackchannelTimeout, - MaxResponseContentBufferSize = 1024 * 1024 * 10 - }; - // 10 MB - var cm = new ConfigurationManager(Options.MetadataAddress, httpClient); - - _configuration = await cm.GetConfigurationAsync(CancellationToken.None); - } var baseUri = Request.Scheme + @@ -244,9 +238,15 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop properties.RedirectUri = currentUri; } + var configuration = await GetWsFederationConfiguration(); + if (configuration == null) + { + this.Response.StatusCode = 401; + return ; + } var wsFederationMessage = new WsFederationMessage { - IssuerAddress = _configuration.TokenEndpoint ?? string.Empty, + IssuerAddress = configuration.TokenEndpoint ?? string.Empty, Wtrealm = Options.Wtrealm, Wctx = $"{WsFederationAuthenticationDefaults.WctxKey}={Uri.EscapeDataString(Options.StateDataFormat.Protect(properties))}", @@ -283,8 +283,6 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop Response.Redirect(redirectUri); } - - /// public async Task SignOutAsync(AuthenticationProperties properties) { @@ -296,14 +294,15 @@ public async Task SignOutAsync(AuthenticationProperties properties) Logger.LogTrace($"Entering {nameof(WsFederationAuthenticationHandler)}'s HandleSignOutAsync"); - if (_configuration == null && Options.ConfigurationManager != null) + var configuration = await GetWsFederationConfiguration(); + if (configuration == null) { - _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + return; } var wsFederationMessage = new WsFederationMessage { - IssuerAddress = _configuration.TokenEndpoint ?? string.Empty, + IssuerAddress = configuration.TokenEndpoint ?? string.Empty, Wtrealm = Options.Wtrealm, Wa = WsFederationActions.SignOut }; From 72cd5c3300805c9273c8bb22a28b6e8427123555 Mon Sep 17 00:00:00 2001 From: Michael Farr Date: Fri, 25 Aug 2017 17:00:48 +1000 Subject: [PATCH 8/8] Scatter LogTrace in setup code. Note: you must do .ConfigureLogging(a=>a.SetMinimumLevel(LogLevel.Trace)) in your Program.Main to get these --- .../WsFederationAuthenticationHandler.cs | 25 ++++++++++--------- .../WsFederationPostConfigureOptions.cs | 7 +++++- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs index 684d520..0915b1c 100644 --- a/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs +++ b/AspNetCore.Authentication.WsFederation/WsFederationAuthenticationHandler.cs @@ -26,16 +26,20 @@ public WsFederationAuthenticationHandler(IOptionsMonitor GetWsFederationConfiguration() { + Logger.LogTrace("GetWsFederationConfiguration start"); if (Options.Configuration == null && Options.ConfigurationManager == null) { - Logger.LogCritical("Configuration and ConfigurationManager are both null. WsFederationPostConfigureOptions should at least configure ConfigurationManager."); + Logger.LogCritical("Configuration and ConfigurationManager are both null. WsFederationPostConfigureOptions should at least configure ConfigurationManager."); } if (Options.ConfigurationManager != null) { + Logger.LogTrace("GetWsFederationConfiguration GetConfigurationAsync start"); //in theory ConfigurationManager is caching and refreshing this data appropriately, so we dont need to hold on to an instance of this. var configuration = await Options.ConfigurationManager.GetConfigurationAsync(CancellationToken.None); + Logger.LogTrace($"GetWsFederationConfiguration GetConfigurationAsync complete with configuration not null: {configuration != null}"); return configuration; } + Logger.LogTrace($"GetWsFederationConfiguration complete with Configuration not null: {Options.Configuration != null}"); return Options.Configuration; } @@ -53,18 +57,17 @@ protected override Task CreateEventsAsync() public override Task ShouldHandleRequestAsync() { - return Task.FromResult(Options.CallbackPath.HasValue && Options.CallbackPath.Equals(Request.Path, StringComparison.OrdinalIgnoreCase)); + var handle = Options.CallbackPath.HasValue && + Options.CallbackPath.Equals(Request.Path, StringComparison.OrdinalIgnoreCase); + Logger.LogTrace($"ShouldHandleRequestAsync({Request.Path}) = {handle} "); + return Task.FromResult(handle); } - /// - /// Authenticate the user identity with the identity provider. - /// The method process the request on the endpoint defined by CallbackPath. - /// + /// protected override async Task HandleRemoteAuthenticateAsync() { // Allow login to be constrained to a specific path. if (!await ShouldHandleRequestAsync()) - //if (Options.CallbackPath.HasValue && !Options.CallbackPath.Equals(Request.Path, StringComparison.OrdinalIgnoreCase)) { // Not for us. Logger.LogDebug($"Skipping {Options.CallbackPath} != {Request.Path}"); @@ -77,8 +80,7 @@ protected override async Task HandleRemoteAuthenticateAsync if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(Request.ContentType) // May have media/type; charset=utf-8, allow partial match. - && - Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) + && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) && Request.Body.CanRead) { if (!Request.Body.CanSeek) @@ -182,8 +184,7 @@ protected override async Task HandleRemoteAuthenticateAsync ticket.Properties.AllowRefresh = false; } - var securityTokenValidatedNotification = await RunSecurityTokenValidatedEventAsync(wsFederationMessage, - ticket); + var securityTokenValidatedNotification = await RunSecurityTokenValidatedEventAsync(wsFederationMessage, ticket); if (securityTokenValidatedNotification.Result != null && securityTokenValidatedNotification.Result.Handled) { return HandleRequestResult.Success(securityTokenValidatedNotification.Result.Ticket); @@ -242,7 +243,7 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop if (configuration == null) { this.Response.StatusCode = 401; - return ; + return; } var wsFederationMessage = new WsFederationMessage { diff --git a/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs b/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs index cfe8d41..a3f4e61 100644 --- a/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs +++ b/AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Extensions; using Microsoft.IdentityModel.Protocols; @@ -13,15 +14,19 @@ namespace AspNetCore.Authentication.WsFederation public class WsFederationPostConfigureOptions : IPostConfigureOptions { private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly ILogger _logger; - public WsFederationPostConfigureOptions(IDataProtectionProvider dataProtectionProvider) + public WsFederationPostConfigureOptions(IDataProtectionProvider dataProtectionProvider, ILogger logger) { _dataProtectionProvider = dataProtectionProvider; + _logger = logger; } private void ApplyDefaults(WsFederationAuthenticationOptions wsFederationAuthenticationOptions) { + _logger.LogTrace("ConfigureOptions start"); ConfigureOptions(_dataProtectionProvider, wsFederationAuthenticationOptions); + _logger.LogTrace($"ConfigureOptions complete, Audience: {wsFederationAuthenticationOptions?.TokenValidationParameters?.ValidAudience}, AuthType: {wsFederationAuthenticationOptions?.TokenValidationParameters?.AuthenticationType}, IsConfigurationManager not null:{wsFederationAuthenticationOptions?.ConfigurationManager != null}, CallbackPath {wsFederationAuthenticationOptions?.CallbackPath}"); } public void PostConfigure(string name, WsFederationAuthenticationOptions options) {