Add POST support for OpenID Connect authorization and logout requests

This commit is contained in:
Kévin Chalet 2015-08-07 23:41:02 +02:00
parent fa39144937
commit d9b3ea2a54
6 changed files with 152 additions and 15 deletions

View File

@ -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 =>
{

View File

@ -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 = @"<input type=""hidden"" name=""{0}"" value=""{1}"" />";
private const string HtmlFormFormat = @"<!doctype html>
<html>
<head>
<title>Please wait while you're being redirected to the identity provider</title>
</head>
<body>
<form name=""form"" method=""post"" action=""{0}"">
{1}
<noscript>Click here to finish the process: <input type=""submit"" /></noscript>
</form>
<script>document.form.submit();</script>
</body>
</html>";
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;
}
/// <summary>

View File

@ -0,0 +1,21 @@
namespace Microsoft.AspNet.Authentication.OpenIdConnect
{
/// <summary>
/// Lists the different authentication methods used to
/// redirect the user agent to the identity provider.
/// </summary>
public enum OpenIdConnectAuthenticationMethod
{
/// <summary>
/// Emits a 302 response to redirect the user agent to
/// the OpenID Connect provider using a GET request.
/// </summary>
RedirectGet = 0,
/// <summary>
/// Emits an HTML form to redirect the user agent to
/// the OpenID Connect provider using a POST request.
/// </summary>
FormPost = 1
}
}

View File

@ -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(

View File

@ -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
/// </summary>
public bool RefreshOnIssuerKeyNotFound { get; set; } = true;
/// <summary>
/// Gets or sets the method used to redirect the user agent to the identity provider.
/// </summary>
public OpenIdConnectAuthenticationMethod AuthenticationMethod { get; set; }
/// <summary>
/// Gets or sets the 'resource'.
/// </summary>
@ -242,5 +248,10 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// This is enabled by default.
/// </summary>
public bool UseTokenLifetime { get; set; } = true;
/// <summary>
/// Gets or sets the <see cref="IHtmlEncoder"/> used to sanitize HTML outputs.
/// </summary>
public IHtmlEncoder HtmlEncoder { get; set; }
}
}

View File

@ -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()
{