Add a 'sid' check to the OIDC middleware to prevent unsolicited logout when possible

This commit is contained in:
Kévin Chalet 2016-05-31 02:05:48 +02:00 committed by Chris R
parent e299695974
commit 56dca7e0bc
6 changed files with 104 additions and 23 deletions

View File

@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -42,6 +41,7 @@ namespace OpenIdConnectSample
public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory)
{
loggerfactory.AddConsole(LogLevel.Information);
loggerfactory.AddDebug(LogLevel.Information);
// Simple error page
app.Use(async (context, next) =>
@ -98,11 +98,11 @@ namespace OpenIdConnectSample
if (context.Request.Path.Equals("/signout-remote"))
{
// Redirects
await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await context.Authentication.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties()
{
RedirectUri = "/signedout"
});
await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return;
}
if (context.Request.Path.Equals("/Account/AccessDenied"))

View File

@ -9,6 +9,7 @@
"Microsoft.Extensions.Configuration.UserSecrets": "1.0.0-*",
"Microsoft.Extensions.FileProviders.Embedded": "1.0.0-*",
"Microsoft.Extensions.Logging.Console": "1.0.0-*",
"Microsoft.Extensions.Logging.Debug": "1.0.0-*",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0-*"
},
"frameworks": {

View File

@ -3,14 +3,19 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
{
public class RemoteSignOutContext : BaseOpenIdConnectContext
{
public RemoteSignOutContext(HttpContext context, OpenIdConnectOptions options)
public RemoteSignOutContext(
HttpContext context,
OpenIdConnectOptions options,
OpenIdConnectMessage message)
: base(context, options)
{
ProtocolMessage = message;
}
}
}

View File

@ -50,6 +50,8 @@ namespace Microsoft.Extensions.Logging
private static Action<ILogger, Exception> _remoteSignOutHandledResponse;
private static Action<ILogger, Exception> _remoteSignOutSkipped;
private static Action<ILogger, Exception> _remoteSignOut;
private static Action<ILogger, Exception> _remoteSignOutSessionIdMissing;
private static Action<ILogger, Exception> _remoteSignOutSessionIdInvalid;
static LoggingExtensions()
{
@ -226,6 +228,16 @@ namespace Microsoft.Extensions.Logging
eventId: 46,
logLevel: LogLevel.Information,
formatString: "Remote signout request processed.");
_remoteSignOutSessionIdMissing = LoggerMessage.Define(
eventId: 47,
logLevel: LogLevel.Error,
formatString: "The remote signout request was ignored because the 'sid' parameter " +
"was missing, which may indicate an unsolicited logout.");
_remoteSignOutSessionIdInvalid = LoggerMessage.Define(
eventId: 48,
logLevel: LogLevel.Error,
formatString: "The remote signout request was ignored because the 'sid' parameter didn't match " +
"the expected value, which may indicate an unsolicited logout.");
}
public static void UpdatingConfiguration(this ILogger logger)
@ -442,5 +454,15 @@ namespace Microsoft.Extensions.Logging
{
_remoteSignOut(logger, null);
}
public static void RemoteSignOutSessionIdMissing(this ILogger logger)
{
_remoteSignOutSessionIdMissing(logger, null);
}
public static void RemoteSignOutSessionIdInvalid(this ILogger logger)
{
_remoteSignOutSessionIdInvalid(logger, null);
}
}
}

View File

@ -66,29 +66,76 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
{
if (Options.RemoteSignOutPath.HasValue && Options.RemoteSignOutPath == Request.Path)
{
var remoteSignOutContext = new RemoteSignOutContext(Context, Options);
await Options.Events.RemoteSignOut(remoteSignOutContext);
if (remoteSignOutContext.HandledResponse)
{
Logger.RemoteSignOutHandledResponse();
return true;
}
if (remoteSignOutContext.Skipped)
{
Logger.RemoteSignOutSkipped();
return false;
}
Logger.RemoteSignOut();
// We've received a remote sign-out request
await Context.Authentication.SignOutAsync(Options.SignOutScheme ?? Options.SignInScheme);
return true;
return await HandleRemoteSignOutAsync();
}
return await base.HandleRequestAsync();
}
protected virtual async Task<bool> HandleRemoteSignOutAsync()
{
OpenIdConnectMessage message = null;
if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
{
message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
}
// assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small.
else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrEmpty(Request.ContentType)
// May have media/type; charset=utf-8, allow partial match.
&& Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)
&& Request.Body.CanRead) {
var form = await Request.ReadFormAsync();
message = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
}
var remoteSignOutContext = new RemoteSignOutContext(Context, Options, message);
await Options.Events.RemoteSignOut(remoteSignOutContext);
if (remoteSignOutContext.HandledResponse)
{
Logger.RemoteSignOutHandledResponse();
return true;
}
if (remoteSignOutContext.Skipped)
{
Logger.RemoteSignOutSkipped();
return false;
}
if (message == null)
{
return false;
}
// Try to extract the session identifier from the authentication ticket persisted by the sign-in handler.
// If the identifier cannot be found, bypass the session identifier checks: this may indicate that the
// authentication cookie was already cleared, that the session identifier was lost because of a lossy
// external/application cookie conversion or that the identity provider doesn't support sessions.
var sid = (await Context.Authentication.AuthenticateAsync(Options.SignOutScheme))?.FindFirst("sid")?.Value;
if (!string.IsNullOrEmpty(sid))
{
// Ensure a 'sid' parameter was sent by the identity provider.
if (string.IsNullOrEmpty(message.GetParameter("sid")))
{
Logger.RemoteSignOutSessionIdMissing();
return true;
}
// Ensure the 'sid' parameter corresponds to the 'sid' stored in the authentication ticket.
if (!string.Equals(sid, message.GetParameter("sid"), StringComparison.Ordinal))
{
Logger.RemoteSignOutSessionIdInvalid();
return true;
}
}
Logger.RemoteSignOut();
// We've received a remote sign-out request
await Context.Authentication.SignOutAsync(Options.SignOutScheme);
return true;
}
/// <summary>
/// Handles Signout
/// </summary>
@ -132,7 +179,9 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
message.PostLogoutRedirectUri = logoutRedirectUri;
}
message.IdTokenHint = await Context.Authentication.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
// Attach the identity token to the logout request when possible.
message.IdTokenHint = await Context.Authentication.GetTokenAsync(Options.SignOutScheme, OpenIdConnectParameterNames.IdToken);
var redirectContext = new RedirectContext(Context, Options, properties)
{
ProtocolMessage = message

View File

@ -100,6 +100,10 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
{
throw new ArgumentException("Options.SignInScheme is required.");
}
if (string.IsNullOrEmpty(Options.SignOutScheme))
{
Options.SignOutScheme = Options.SignInScheme;
}
HtmlEncoder = htmlEncoder;