diff --git a/samples/OpenIdConnectSample/Startup.cs b/samples/OpenIdConnectSample/Startup.cs index 6beaa0a20b..e9836506a3 100644 --- a/samples/OpenIdConnectSample/Startup.cs +++ b/samples/OpenIdConnectSample/Startup.cs @@ -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")) diff --git a/samples/OpenIdConnectSample/project.json b/samples/OpenIdConnectSample/project.json index 32e19ca40e..c507160fec 100644 --- a/samples/OpenIdConnectSample/project.json +++ b/samples/OpenIdConnectSample/project.json @@ -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": { diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs index b5077e035d..a76dc9e592 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs @@ -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; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs index ef1d5d83b9..d0f3e12d90 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs @@ -50,6 +50,8 @@ namespace Microsoft.Extensions.Logging private static Action _remoteSignOutHandledResponse; private static Action _remoteSignOutSkipped; private static Action _remoteSignOut; + private static Action _remoteSignOutSessionIdMissing; + private static Action _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); + } } } diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs index c674e21077..a10122b7af 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs @@ -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 HandleRemoteSignOutAsync() + { + OpenIdConnectMessage message = null; + + if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) + { + message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair(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(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; + } + /// /// Handles Signout /// @@ -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 diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectMiddleware.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectMiddleware.cs index b08f8e944c..8d880d0d90 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectMiddleware.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectMiddleware.cs @@ -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;