From d6763bd77c931aeff1024cafd03255abfe55b753 Mon Sep 17 00:00:00 2001 From: Chris R Date: Wed, 25 May 2016 16:00:57 -0700 Subject: [PATCH] #423 Support distributed sign-out. --- samples/OpenIdConnectSample/Program.cs | 32 +++++++++++++++--- .../Properties/launchSettings.json | 9 ++--- samples/OpenIdConnectSample/Startup.cs | 19 +++++++++++ .../compiler/resources/cert.pfx | Bin 0 -> 2483 bytes samples/OpenIdConnectSample/project.json | 2 ++ samples/SocialSample/Program.cs | 3 -- .../Events/IOpenIdConnectEvents.cs | 5 +++ .../Events/OpenIdConnectEvents.cs | 7 ++++ .../Events/RemoteSignoutContext.cs | 16 +++++++++ .../LoggingExtensions.cs | 30 ++++++++++++++++ .../OpenIdConnectHandler.cs | 27 +++++++++++++++ .../OpenIdConnectOptions.cs | 12 +++++++ 12 files changed, 150 insertions(+), 12 deletions(-) create mode 100644 samples/OpenIdConnectSample/compiler/resources/cert.pfx create mode 100644 src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs diff --git a/samples/OpenIdConnectSample/Program.cs b/samples/OpenIdConnectSample/Program.cs index a81f11dfb0..fe77dd1a7c 100644 --- a/samples/OpenIdConnectSample/Program.cs +++ b/samples/OpenIdConnectSample/Program.cs @@ -1,6 +1,8 @@ using System.IO; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.FileProviders; namespace OpenIdConnectSample { @@ -8,11 +10,13 @@ namespace OpenIdConnectSample { public static void Main(string[] args) { - var config = new ConfigurationBuilder().AddEnvironmentVariables("ASPNETCORE_").Build(); - var host = new WebHostBuilder() - .UseKestrel() - .UseConfiguration(config) + .UseKestrel(options => + { + //Configure SSL + var serverCertificate = LoadCertificate(); + options.UseHttps(serverCertificate); + }) .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup() @@ -20,5 +24,23 @@ namespace OpenIdConnectSample host.Run(); } + + private static X509Certificate2 LoadCertificate() + { + var assembly = typeof(Startup).GetTypeInfo().Assembly; + var embeddedFileProvider = new EmbeddedFileProvider(assembly, "OpenIdConnectSample"); + var certificateFileInfo = embeddedFileProvider.GetFileInfo("compiler/resources/cert.pfx"); + using (var certificateStream = certificateFileInfo.CreateReadStream()) + { + byte[] certificatePayload; + using (var memoryStream = new MemoryStream()) + { + certificateStream.CopyTo(memoryStream); + certificatePayload = memoryStream.ToArray(); + } + + return new X509Certificate2(certificatePayload, "testPassword"); + } + } } } diff --git a/samples/OpenIdConnectSample/Properties/launchSettings.json b/samples/OpenIdConnectSample/Properties/launchSettings.json index 557b6921e2..48610115fa 100644 --- a/samples/OpenIdConnectSample/Properties/launchSettings.json +++ b/samples/OpenIdConnectSample/Properties/launchSettings.json @@ -4,13 +4,14 @@ "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:42023", - "sslPort": 0 + "sslPort": 44318 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "launchUrl": "https://localhost:44318/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -18,10 +19,10 @@ "OpenIdConnectSample": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "http://localhost:42023", + "launchUrl": "https://localhost:44318/", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_SERVER.URLS": "http://localhost:42023" + "ASPNETCORE_URLS": "https://localhost:44318/", + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/samples/OpenIdConnectSample/Startup.cs b/samples/OpenIdConnectSample/Startup.cs index 3fffe9ce69..6beaa0a20b 100644 --- a/samples/OpenIdConnectSample/Startup.cs +++ b/samples/OpenIdConnectSample/Startup.cs @@ -78,6 +78,14 @@ namespace OpenIdConnectSample app.Run(async context => { + if (context.Request.Path.Equals("/signedout")) + { + context.Response.ContentType = "text/html"; + await context.Response.WriteAsync($"You have been signed out.
{Environment.NewLine}"); + await context.Response.WriteAsync("Sign In"); + await context.Response.WriteAsync($""); + return; + } if (context.Request.Path.Equals("/signout")) { await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); @@ -87,6 +95,16 @@ namespace OpenIdConnectSample await context.Response.WriteAsync($""); return; } + if (context.Request.Path.Equals("/signout-remote")) + { + // Redirects + await context.Authentication.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties() + { + RedirectUri = "/signedout" + }); + await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + return; + } if (context.Request.Path.Equals("/Account/AccessDenied")) { await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); @@ -134,6 +152,7 @@ namespace OpenIdConnectSample } await context.Response.WriteAsync("Restricted
"); await context.Response.WriteAsync("Sign Out
"); + await context.Response.WriteAsync("Sign Out Remote
"); await context.Response.WriteAsync($""); }); } diff --git a/samples/OpenIdConnectSample/compiler/resources/cert.pfx b/samples/OpenIdConnectSample/compiler/resources/cert.pfx new file mode 100644 index 0000000000000000000000000000000000000000..7118908c2d730670c16e9f8b2c532a262c951989 GIT binary patch literal 2483 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 literal 0 HcmV?d00001 diff --git a/samples/OpenIdConnectSample/project.json b/samples/OpenIdConnectSample/project.json index 5f0744e0b7..32e19ca40e 100644 --- a/samples/OpenIdConnectSample/project.json +++ b/samples/OpenIdConnectSample/project.json @@ -5,7 +5,9 @@ "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.0.0-*", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-*", "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*", + "Microsoft.AspNetCore.Server.Kestrel.Https": "1.0.0-*", "Microsoft.Extensions.Configuration.UserSecrets": "1.0.0-*", + "Microsoft.Extensions.FileProviders.Embedded": "1.0.0-*", "Microsoft.Extensions.Logging.Console": "1.0.0-*", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0-*" }, diff --git a/samples/SocialSample/Program.cs b/samples/SocialSample/Program.cs index 386819e6f5..f3cad66ad3 100644 --- a/samples/SocialSample/Program.cs +++ b/samples/SocialSample/Program.cs @@ -11,8 +11,6 @@ namespace SocialSample { public static void Main(string[] args) { - var config = new ConfigurationBuilder().AddEnvironmentVariables("ASPNETCORE_").Build(); - var host = new WebHostBuilder() .UseKestrel(options => { @@ -20,7 +18,6 @@ namespace SocialSample var serverCertificate = LoadCertificate(); options.UseHttps(serverCertificate); }) - .UseConfiguration(config) .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup() diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/IOpenIdConnectEvents.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/IOpenIdConnectEvents.cs index 57600cee8d..128fa08a3e 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/IOpenIdConnectEvents.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/IOpenIdConnectEvents.cs @@ -35,6 +35,11 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect /// Task RedirectToIdentityProviderForSignOut(RedirectContext context); + /// + /// Invoked when a request is received on the RemoteSignOutPath. + /// + Task RemoteSignOut(RemoteSignOutContext context); + /// /// Invoked after "authorization code" is redeemed for tokens at the token endpoint. /// diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs index 9893b72072..42d35b7982 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs @@ -36,6 +36,11 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect /// public Func OnRedirectToIdentityProviderForSignOut { get; set; } = context => Task.FromResult(0); + /// + /// Invoked when a request is received on the RemoteSignOutPath. + /// + public Func OnRemoteSignOut { get; set; } = context => Task.FromResult(0); + /// /// Invoked after "authorization code" is redeemed for tokens at the token endpoint. /// @@ -61,6 +66,8 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect public virtual Task RedirectToIdentityProviderForSignOut(RedirectContext context) => OnRedirectToIdentityProviderForSignOut(context); + public virtual Task RemoteSignOut(RemoteSignOutContext context) => OnRemoteSignOut(context); + public virtual Task TokenResponseReceived(TokenResponseReceivedContext context) => OnTokenResponseReceived(context); public virtual Task TokenValidated(TokenValidatedContext context) => OnTokenValidated(context); diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs new file mode 100644 index 0000000000..b5077e035d --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs @@ -0,0 +1,16 @@ +// 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.Builder; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + public class RemoteSignOutContext : BaseOpenIdConnectContext + { + public RemoteSignOutContext(HttpContext context, OpenIdConnectOptions options) + : base(context, options) + { + } + } +} \ 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 ff580ff266..ef1d5d83b9 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs @@ -47,6 +47,9 @@ namespace Microsoft.Extensions.Logging private static Action _invalidSecurityTokenType; private static Action _unableToValidateIdToken; private static Action _postAuthenticationLocalRedirect; + private static Action _remoteSignOutHandledResponse; + private static Action _remoteSignOutSkipped; + private static Action _remoteSignOut; static LoggingExtensions() { @@ -211,6 +214,18 @@ namespace Microsoft.Extensions.Logging eventId: 43, logLevel: LogLevel.Error, formatString: "Unable to read the 'id_token', no suitable ISecurityTokenValidator was found for: '{IdToken}'."); + _remoteSignOutHandledResponse = LoggerMessage.Define( + eventId: 44, + logLevel: LogLevel.Debug, + formatString: "RemoteSignOutContext.HandledResponse"); + _remoteSignOutSkipped = LoggerMessage.Define( + eventId: 45, + logLevel: LogLevel.Debug, + formatString: "RemoteSignOutContext.Skipped"); + _remoteSignOut = LoggerMessage.Define( + eventId: 46, + logLevel: LogLevel.Information, + formatString: "Remote signout request processed."); } public static void UpdatingConfiguration(this ILogger logger) @@ -412,5 +427,20 @@ namespace Microsoft.Extensions.Logging { _postAuthenticationLocalRedirect(logger, redirectUri, 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); + } } } diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs index f85ba9c146..e23173ed18 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs @@ -62,6 +62,33 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect HtmlEncoder = htmlEncoder; } + public override async Task HandleRequestAsync() + { + 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 base.HandleRequestAsync(); + } + /// /// Handles Signout /// diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs index cdbc9e0f0d..c9b614acde 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs @@ -50,6 +50,7 @@ namespace Microsoft.AspNetCore.Builder AutomaticChallenge = true; DisplayName = OpenIdConnectDefaults.Caption; CallbackPath = new PathString("/signin-oidc"); + RemoteSignOutPath = new PathString("/signout-oidc"); Events = new OpenIdConnectEvents(); Scope.Add("openid"); Scope.Add("profile"); @@ -155,6 +156,17 @@ namespace Microsoft.AspNetCore.Builder /// public ICollection Scope { get; } = new HashSet(); + /// + /// Requests received on this path will cause the middleware to invoke SignOut using the SignInScheme. + /// + public PathString RemoteSignOutPath { get; set; } + + /// + /// The Authentication Scheme to use with SignOut on the SignOutPath. SignInScheme will be used if this + /// is not set. + /// + public string SignOutScheme { get; set; } + /// /// Gets or sets the type used to secure data handled by the middleware. ///