Skip to content
This repository was archived by the owner on Dec 13, 2018. It is now read-only.

Implement WsFed remote signout cleanup #1531

Merged
merged 1 commit into from
Nov 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.Protocols.WsFederation;

namespace Microsoft.AspNetCore.Authentication.WsFederation
{
/// <summary>
/// An event context for RemoteSignOut.
/// </summary>
public class RemoteSignOutContext : RemoteAuthenticationContext<WsFederationOptions>
{
/// <summary>
///
/// </summary>
/// <param name="context"></param>
/// <param name="scheme"></param>
/// <param name="options"></param>
/// <param name="message"></param>
public RemoteSignOutContext(HttpContext context, AuthenticationScheme scheme, WsFederationOptions options, WsFederationMessage message)
: base(context, scheme, options, new AuthenticationProperties())
=> ProtocolMessage = message;

/// <summary>
/// The signout message.
/// </summary>
public WsFederationMessage ProtocolMessage { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ public class WsFederationEvents : RemoteAuthenticationEvents
/// </summary>
public Func<RedirectContext, Task> OnRedirectToIdentityProvider { get; set; } = context => Task.CompletedTask;

/// <summary>
/// Invoked when a wsignoutcleanup request is received at the RemoteSignOutPath endpoint.
/// </summary>
public Func<RemoteSignOutContext, Task> OnRemoteSignOut { get; set; } = context => Task.CompletedTask;

/// <summary>
/// Invoked with the security token that has been extracted from the protocol message.
/// </summary>
Expand All @@ -51,6 +56,11 @@ public class WsFederationEvents : RemoteAuthenticationEvents
/// </summary>
public virtual Task RedirectToIdentityProvider(RedirectContext context) => OnRedirectToIdentityProvider(context);

/// <summary>
/// Invoked when a wsignoutcleanup request is received at the RemoteSignOutPath endpoint.
/// </summary>
public virtual Task RemoteSignOut(RemoteSignOutContext context) => OnRemoteSignOut(context);

/// <summary>
/// Invoked with the security token that has been extracted from the protocol message.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ internal static class LoggingExtensions
private static Action<ILogger, Exception> _signInWithoutToken;
private static Action<ILogger, Exception> _exceptionProcessingMessage;
private static Action<ILogger, string, Exception> _malformedRedirectUri;
private static Action<ILogger, Exception> _remoteSignOutHandledResponse;
private static Action<ILogger, Exception> _remoteSignOutSkipped;
private static Action<ILogger, Exception> _remoteSignOut;

static LoggingExtensions()
{
Expand All @@ -30,6 +33,18 @@ static LoggingExtensions()
eventId: 4,
logLevel: LogLevel.Warning,
formatString: "The sign-out redirect URI '{0}' is malformed.");
_remoteSignOutHandledResponse = LoggerMessage.Define(
eventId: 5,
logLevel: LogLevel.Debug,
formatString: "RemoteSignOutContext.HandledResponse");
_remoteSignOutSkipped = LoggerMessage.Define(
eventId: 6,
logLevel: LogLevel.Debug,
formatString: "RemoteSignOutContext.Skipped");
_remoteSignOut = LoggerMessage.Define(
eventId: 7,
logLevel: LogLevel.Information,
formatString: "Remote signout request processed.");
}

public static void SignInWithoutWresult(this ILogger logger)
Expand All @@ -51,5 +66,20 @@ public static void MalformedRedirectUri(this ILogger logger, string uri)
{
_malformedRedirectUri(logger, uri, null);
}

public static void RemoteSignOutHandledResponse(this ILogger logger)
{
_remoteSignOutHandledResponse(logger, null);
}

public static void RemoteSignOutSkipped(this ILogger logger)
{
_remoteSignOutSkipped(logger, null);
}

public static void RemoteSignOut(this ILogger logger)
{
_remoteSignOut(logger, null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ public WsFederationHandler(IOptionsMonitor<WsFederationOptions> options, ILogger
/// <returns>A new instance of the events instance.</returns>
protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new WsFederationEvents());

/// <summary>
/// Overridden to handle remote signout requests
/// </summary>
/// <returns></returns>
public override Task<bool> HandleRequestAsync()
{
if (Options.RemoteSignOutPath.HasValue && Options.RemoteSignOutPath == Request.Path)
{
return HandleRemoteSignOutAsync();
}

return base.HandleRequestAsync();
}

/// <summary>
/// Handles Challenge
/// </summary>
Expand Down Expand Up @@ -359,6 +373,49 @@ public async virtual Task SignOutAsync(AuthenticationProperties properties)
}
}

/// <summary>
/// Handles requests to the RemoteSignOutPath and signs out the user.
/// </summary>
/// <returns></returns>
protected virtual async Task<bool> HandleRemoteSignOutAsync()
{
WsFederationMessage message = null;

if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
{
message = new WsFederationMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
}

var remoteSignOutContext = new RemoteSignOutContext(Context, Scheme, Options, message);
await Events.RemoteSignOut(remoteSignOutContext);

if (remoteSignOutContext.Result != null)
{
if (remoteSignOutContext.Result.Handled)
{
Logger.RemoteSignOutHandledResponse();
return true;
}
if (remoteSignOutContext.Result.Skipped)
{
Logger.RemoteSignOutSkipped();
return false;
}
}

if (message == null
|| !string.Equals(message.Wa, WsFederationConstants.WsFederationActions.SignOutCleanup, StringComparison.OrdinalIgnoreCase))
{
return false;
}

Logger.RemoteSignOut();

// We've received a remote sign-out request
await Context.SignOutAsync(Options.SignOutScheme);
return true;
}

/// <summary>
/// Build a redirect path if the given path is a relative path.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.WsFederation;
using Microsoft.IdentityModel.Tokens;
Expand Down Expand Up @@ -32,6 +33,7 @@ public class WsFederationOptions : RemoteAuthenticationOptions
public WsFederationOptions()
{
CallbackPath = "/signin-wsfed";
RemoteSignOutPath = "/signout-wsfed";
Events = new WsFederationEvents();
}

Expand Down Expand Up @@ -159,5 +161,16 @@ public TokenValidationParameters TokenValidationParameters
/// However, that flow is susceptible to XSRF and other attacks so it is disabled here by default.
/// </summary>
public bool AllowUnsolicitedLogins { get; set; }

/// <summary>
/// Requests received on this path will cause the handler to invoke SignOut using the SignInScheme.
/// </summary>
public PathString RemoteSignOutPath { get; set; }

/// <summary>
/// The Authentication Scheme to use with SignOutAsync from RemoteSignOutPath. SignInScheme will be used if this
/// is not set.
/// </summary>
public string SignOutScheme { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ public WsFederationPostConfigureOptions(IDataProtectionProvider dataProtection)
public void PostConfigure(string name, WsFederationOptions options)
{
options.DataProtectionProvider = options.DataProtectionProvider ?? _dp;


if (string.IsNullOrEmpty(options.SignOutScheme))
{
options.SignOutScheme = options.SignInScheme;
}

if (options.StateDataFormat == null)
{
var dataProtector = options.DataProtectionProvider.CreateProtector(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,20 @@ public async Task InvalidTokenIsRejected()
Assert.Equal("AuthenticationFailed", await response.Content.ReadAsStringAsync());
}

[Fact]
public async Task RemoteSignoutRequestTriggersSignout()
{
var httpClient = CreateClient();

var response = await httpClient.GetAsync("/signout-wsfed?wa=wsignoutcleanup1.0");
response.EnsureSuccessStatusCode();

var cookie = response.Headers.GetValues(HeaderNames.SetCookie).Single();
Assert.Equal(".AspNetCore.Cookies=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; samesite=lax", cookie);
Assert.Equal("OnRemoteSignOut", response.Headers.GetValues("EventHeader").Single());
Assert.Equal("", await response.Content.ReadAsStringAsync());
}

[Fact]
public async Task EventsResolvedFromDI()
{
Expand Down Expand Up @@ -251,6 +265,11 @@ private HttpClient CreateClient(bool allowUnsolicited = false)
context.HttpContext.Request.Path = new PathString("/AuthenticationFailed");
context.SkipHandler();
return Task.FromResult(0);
},
OnRemoteSignOut = context =>
{
context.Response.Headers["EventHeader"] = "OnRemoteSignOut";
return Task.FromResult(0);
}
};
});
Expand Down