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 0000000000..7118908c2d Binary files /dev/null and b/samples/OpenIdConnectSample/compiler/resources/cert.pfx differ 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. ///