From d9b3ea2a54944fe0310beae584b7cce2e95cbe61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 7 Aug 2015 23:41:02 +0200 Subject: [PATCH] Add POST support for OpenID Connect authorization and logout requests --- samples/OpenIdConnectSample/Startup.cs | 10 +- .../OpenIdConnectAuthenticationHandler.cs | 104 ++++++++++++++++-- .../OpenIdConnectAuthenticationMethod.cs | 21 ++++ .../OpenIdConnectAuthenticationMiddleware.cs | 5 + .../OpenIdConnectAuthenticationOptions.cs | 11 ++ .../OpenIdConnectMiddlewareTests.cs | 16 +++ 6 files changed, 152 insertions(+), 15 deletions(-) create mode 100644 src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMethod.cs diff --git a/samples/OpenIdConnectSample/Startup.cs b/samples/OpenIdConnectSample/Startup.cs index 00652c32f5..26c9129e07 100644 --- a/samples/OpenIdConnectSample/Startup.cs +++ b/samples/OpenIdConnectSample/Startup.cs @@ -31,11 +31,11 @@ namespace OpenIdConnectSample }); app.UseOpenIdConnectAuthentication(options => - { - options.ClientId = "fe78e0b4-6fe7-47e6-812c-fb75cee266a4"; - options.Authority = "https://login.windows.net/cyrano.onmicrosoft.com"; - options.RedirectUri = "http://localhost:42023"; - }); + { + options.ClientId = "fe78e0b4-6fe7-47e6-812c-fb75cee266a4"; + options.Authority = "https://login.windows.net/cyrano.onmicrosoft.com"; + options.RedirectUri = "http://localhost:42023"; + }); app.Run(async context => { diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs index 6b457df156..6a209dd45d 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; @@ -19,6 +20,7 @@ using Microsoft.Framework.Caching.Distributed; using Microsoft.Framework.Internal; using Microsoft.Framework.Logging; using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.Net.Http.Headers; using Newtonsoft.Json.Linq; namespace Microsoft.AspNet.Authentication.OpenIdConnect @@ -30,6 +32,22 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect { private const string NonceProperty = "N"; private const string UriSchemeDelimiter = "://"; + + private const string InputTagFormat = @""; + private const string HtmlFormFormat = @" + + + Please wait while you're being redirected to the identity provider + + +
+ {1} + +
+ + +"; + private OpenIdConnectConfiguration _configuration; protected HttpClient Backchannel { get; private set; } @@ -93,13 +111,43 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect message = redirectToIdentityProviderNotification.ProtocolMessage; } - var redirectUri = message.CreateLogoutRequestUrl(); - if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + if (Options.AuthenticationMethod == OpenIdConnectAuthenticationMethod.RedirectGet) { - Logger.LogWarning(Resources.OIDCH_0051_RedirectUriLogoutIsNotWellFormed, redirectUri); - } + var redirectUri = message.CreateLogoutRequestUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + Logger.LogWarning(Resources.OIDCH_0051_RedirectUriLogoutIsNotWellFormed, redirectUri); + } - Response.Redirect(redirectUri); + Response.Redirect(redirectUri); + } + else if (Options.AuthenticationMethod == OpenIdConnectAuthenticationMethod.FormPost) + { + var inputs = new StringBuilder(); + foreach (var parameter in message.Parameters) + { + var name = Options.HtmlEncoder.HtmlEncode(parameter.Key); + var value = Options.HtmlEncoder.HtmlEncode(parameter.Value); + + var input = string.Format(CultureInfo.InvariantCulture, InputTagFormat, name, value); + inputs.AppendLine(input); + } + + var issuer = Options.HtmlEncoder.HtmlEncode(message.IssuerAddress); + + var content = string.Format(CultureInfo.InvariantCulture, HtmlFormFormat, issuer, inputs); + var buffer = Encoding.UTF8.GetBytes(content); + + Response.ContentLength = buffer.Length; + Response.ContentType = "text/html;charset=UTF-8"; + + // Emit Cache-Control=no-cache to prevent client caching. + Response.Headers.Set(HeaderNames.CacheControl, "no-cache"); + Response.Headers.Set(HeaderNames.Pragma, "no-cache"); + Response.Headers.Set(HeaderNames.Expires, "-1"); + + await Response.Body.WriteAsync(buffer, 0, buffer.Length); + } } } @@ -218,14 +266,50 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect message.State = Options.StateDataFormat.Protect(properties); - var redirectUri = message.CreateAuthenticationRequestUrl(); - if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + if (Options.AuthenticationMethod == OpenIdConnectAuthenticationMethod.RedirectGet) { - Logger.LogWarning(Resources.OIDCH_0036_UriIsNotWellFormed, redirectUri); + var redirectUri = message.CreateAuthenticationRequestUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + Logger.LogWarning(Resources.OIDCH_0036_UriIsNotWellFormed, redirectUri); + } + + Response.Redirect(redirectUri); + + return true; + } + else if (Options.AuthenticationMethod == OpenIdConnectAuthenticationMethod.FormPost) + { + var inputs = new StringBuilder(); + foreach (var parameter in message.Parameters) + { + var name = Options.HtmlEncoder.HtmlEncode(parameter.Key); + var value = Options.HtmlEncoder.HtmlEncode(parameter.Value); + + var input = string.Format(CultureInfo.InvariantCulture, InputTagFormat, name, value); + inputs.AppendLine(input); + } + + var issuer = Options.HtmlEncoder.HtmlEncode(message.IssuerAddress); + + var content = string.Format(CultureInfo.InvariantCulture, HtmlFormFormat, issuer, inputs); + var buffer = Encoding.UTF8.GetBytes(content); + + Response.ContentLength = buffer.Length; + Response.ContentType = "text/html;charset=UTF-8"; + + // Emit Cache-Control=no-cache to prevent client caching. + Response.Headers.Set(HeaderNames.CacheControl, "no-cache"); + Response.Headers.Set(HeaderNames.Pragma, "no-cache"); + Response.Headers.Set(HeaderNames.Expires, "-1"); + + await Response.Body.WriteAsync(buffer, 0, buffer.Length); + + return true; } - Response.Redirect(redirectUri); - return true; + Logger.LogError("An unsupported authentication method has been configured: {0}", Options.AuthenticationMethod); + return false; } /// diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMethod.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMethod.cs new file mode 100644 index 0000000000..147d576f9b --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMethod.cs @@ -0,0 +1,21 @@ +namespace Microsoft.AspNet.Authentication.OpenIdConnect +{ + /// + /// Lists the different authentication methods used to + /// redirect the user agent to the identity provider. + /// + public enum OpenIdConnectAuthenticationMethod + { + /// + /// Emits a 302 response to redirect the user agent to + /// the OpenID Connect provider using a GET request. + /// + RedirectGet = 0, + + /// + /// Emits an HTML form to redirect the user agent to + /// the OpenID Connect provider using a POST request. + /// + FormPost = 1 + } +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs index e4bef30dbb..b26fd1f5e8 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs @@ -54,6 +54,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect Options.SignInScheme = sharedOptions.Options.SignInScheme; } + if (Options.HtmlEncoder == null) + { + Options.HtmlEncoder = services.GetHtmlEncoder(); + } + if (Options.StateDataFormat == null) { var dataProtector = dataProtectionProvider.CreateProtector( diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs index 70809636cd..4bdfcca994 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs @@ -11,6 +11,7 @@ using System.Security.Claims; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; using Microsoft.Framework.Caching.Distributed; +using Microsoft.Framework.WebEncoders; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; @@ -190,6 +191,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// public bool RefreshOnIssuerKeyNotFound { get; set; } = true; + /// + /// Gets or sets the method used to redirect the user agent to the identity provider. + /// + public OpenIdConnectAuthenticationMethod AuthenticationMethod { get; set; } + /// /// Gets or sets the 'resource'. /// @@ -242,5 +248,10 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// This is enabled by default. /// public bool UseTokenLifetime { get; set; } = true; + + /// + /// Gets or sets the used to sanitize HTML outputs. + /// + public IHtmlEncoder HtmlEncoder { get; set; } } } diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs index 912a61047e..72c82999bf 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs @@ -40,6 +40,22 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect const string Signin = "/signin"; const string Signout = "/signout"; + [Fact] + public async Task ChallengeWillIssueHtmlFormWhenEnabled() + { + var server = CreateServer(options => + { + options.Authority = DefaultAuthority; + options.ClientId = "Test Id"; + options.Configuration = TestUtilities.DefaultOpenIdConnectConfiguration; + options.AuthenticationMethod = OpenIdConnectAuthenticationMethod.FormPost; + }); + var transaction = await SendAsync(server, DefaultHost + Challenge); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + transaction.Response.Content.Headers.ContentType.MediaType.ShouldBe("text/html"); + transaction.ResponseText.ShouldContain("form"); + } + [Fact] public async Task ChallengeWillSetDefaults() {