Add a 'sid' check to the OIDC middleware to prevent unsolicited logout when possible
This commit is contained in:
parent
e299695974
commit
56dca7e0bc
|
|
@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Authentication;
|
using Microsoft.AspNetCore.Http.Authentication;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -42,6 +41,7 @@ namespace OpenIdConnectSample
|
||||||
public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory)
|
public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory)
|
||||||
{
|
{
|
||||||
loggerfactory.AddConsole(LogLevel.Information);
|
loggerfactory.AddConsole(LogLevel.Information);
|
||||||
|
loggerfactory.AddDebug(LogLevel.Information);
|
||||||
|
|
||||||
// Simple error page
|
// Simple error page
|
||||||
app.Use(async (context, next) =>
|
app.Use(async (context, next) =>
|
||||||
|
|
@ -98,11 +98,11 @@ namespace OpenIdConnectSample
|
||||||
if (context.Request.Path.Equals("/signout-remote"))
|
if (context.Request.Path.Equals("/signout-remote"))
|
||||||
{
|
{
|
||||||
// Redirects
|
// Redirects
|
||||||
|
await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
await context.Authentication.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties()
|
await context.Authentication.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties()
|
||||||
{
|
{
|
||||||
RedirectUri = "/signedout"
|
RedirectUri = "/signedout"
|
||||||
});
|
});
|
||||||
await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (context.Request.Path.Equals("/Account/AccessDenied"))
|
if (context.Request.Path.Equals("/Account/AccessDenied"))
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"Microsoft.Extensions.Configuration.UserSecrets": "1.0.0-*",
|
"Microsoft.Extensions.Configuration.UserSecrets": "1.0.0-*",
|
||||||
"Microsoft.Extensions.FileProviders.Embedded": "1.0.0-*",
|
"Microsoft.Extensions.FileProviders.Embedded": "1.0.0-*",
|
||||||
"Microsoft.Extensions.Logging.Console": "1.0.0-*",
|
"Microsoft.Extensions.Logging.Console": "1.0.0-*",
|
||||||
|
"Microsoft.Extensions.Logging.Debug": "1.0.0-*",
|
||||||
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0-*"
|
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0-*"
|
||||||
},
|
},
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,19 @@
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
{
|
{
|
||||||
public class RemoteSignOutContext : BaseOpenIdConnectContext
|
public class RemoteSignOutContext : BaseOpenIdConnectContext
|
||||||
{
|
{
|
||||||
public RemoteSignOutContext(HttpContext context, OpenIdConnectOptions options)
|
public RemoteSignOutContext(
|
||||||
|
HttpContext context,
|
||||||
|
OpenIdConnectOptions options,
|
||||||
|
OpenIdConnectMessage message)
|
||||||
: base(context, options)
|
: base(context, options)
|
||||||
{
|
{
|
||||||
|
ProtocolMessage = message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +50,8 @@ namespace Microsoft.Extensions.Logging
|
||||||
private static Action<ILogger, Exception> _remoteSignOutHandledResponse;
|
private static Action<ILogger, Exception> _remoteSignOutHandledResponse;
|
||||||
private static Action<ILogger, Exception> _remoteSignOutSkipped;
|
private static Action<ILogger, Exception> _remoteSignOutSkipped;
|
||||||
private static Action<ILogger, Exception> _remoteSignOut;
|
private static Action<ILogger, Exception> _remoteSignOut;
|
||||||
|
private static Action<ILogger, Exception> _remoteSignOutSessionIdMissing;
|
||||||
|
private static Action<ILogger, Exception> _remoteSignOutSessionIdInvalid;
|
||||||
|
|
||||||
static LoggingExtensions()
|
static LoggingExtensions()
|
||||||
{
|
{
|
||||||
|
|
@ -226,6 +228,16 @@ namespace Microsoft.Extensions.Logging
|
||||||
eventId: 46,
|
eventId: 46,
|
||||||
logLevel: LogLevel.Information,
|
logLevel: LogLevel.Information,
|
||||||
formatString: "Remote signout request processed.");
|
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)
|
public static void UpdatingConfiguration(this ILogger logger)
|
||||||
|
|
@ -442,5 +454,15 @@ namespace Microsoft.Extensions.Logging
|
||||||
{
|
{
|
||||||
_remoteSignOut(logger, null);
|
_remoteSignOut(logger, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void RemoteSignOutSessionIdMissing(this ILogger logger)
|
||||||
|
{
|
||||||
|
_remoteSignOutSessionIdMissing(logger, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RemoteSignOutSessionIdInvalid(this ILogger logger)
|
||||||
|
{
|
||||||
|
_remoteSignOutSessionIdInvalid(logger, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,29 +66,76 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
{
|
{
|
||||||
if (Options.RemoteSignOutPath.HasValue && Options.RemoteSignOutPath == Request.Path)
|
if (Options.RemoteSignOutPath.HasValue && Options.RemoteSignOutPath == Request.Path)
|
||||||
{
|
{
|
||||||
var remoteSignOutContext = new RemoteSignOutContext(Context, Options);
|
return await HandleRemoteSignOutAsync();
|
||||||
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 base.HandleRequestAsync();
|
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>
|
/// <summary>
|
||||||
/// Handles Signout
|
/// Handles Signout
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -132,7 +179,9 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
message.PostLogoutRedirectUri = logoutRedirectUri;
|
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)
|
var redirectContext = new RedirectContext(Context, Options, properties)
|
||||||
{
|
{
|
||||||
ProtocolMessage = message
|
ProtocolMessage = message
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,10 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Options.SignInScheme is required.");
|
throw new ArgumentException("Options.SignInScheme is required.");
|
||||||
}
|
}
|
||||||
|
if (string.IsNullOrEmpty(Options.SignOutScheme))
|
||||||
|
{
|
||||||
|
Options.SignOutScheme = Options.SignInScheme;
|
||||||
|
}
|
||||||
|
|
||||||
HtmlEncoder = htmlEncoder;
|
HtmlEncoder = htmlEncoder;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue