Browse Source

Implement built-in redirect_uri validation directly in the client

pull/1433/head
Kévin Chalet 4 years ago
parent
commit
190e85ea2c
  1. 3
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  2. 86
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
  3. 99
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs

3
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1667,6 +1667,9 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<data name="ID2137" xml:space="preserve">
<value>An invalid JSON response was returned by the remote HTTP server.</value>
</data>
<data name="ID2138" xml:space="preserve">
<value>The current address doesn't match the address of the redirection endpoint selected during the initial authorization request.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>

86
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs

@ -12,6 +12,7 @@ using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
@ -36,6 +37,7 @@ public static partial class OpenIddictClientAspNetCoreHandlers
* Authentication processing:
*/
ValidateCorrelationCookie.Descriptor,
ValidateRedirectUri.Descriptor,
/*
* Challenge processing:
@ -295,6 +297,90 @@ public static partial class OpenIddictClientAspNetCoreHandlers
}
}
/// <summary>
/// Contains the logic responsible for comparing the current request URL to the redirect_uri stored in the state token.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class ValidateRedirectUri : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateRedirectUri>()
.SetOrder(ValidateCorrelationCookie.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetHttpRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
// Try to resolve the original redirect_uri from the state token. If it cannot be resolved,
// this likely means the authorization request was sent without a redirect_uri attached.
if (!Uri.TryCreate(context.StateTokenPrincipal.GetClaim(Claims.Private.RedirectUri),
UriKind.Absolute, out Uri? address))
{
return default;
}
// Compute the absolute URL of the current request without the query string.
var uri = new Uri(request.Scheme + Uri.SchemeDelimiter + request.Host +
request.PathBase + request.Path, UriKind.Absolute);
// Compare the current HTTP request address to the original redirect_uri. If the two don't
// match, this may indicate a mix-up attack. While the authorization server is expected to
// abort the authorization flow by rejecting the token request that may be eventually sent
// with the original redirect_uri, many servers are known to incorrectly implement this
// redirect_uri validation logic. This check also offers limited protection as it cannot
// prevent the client credentials from being leaked to a malicious authorization server.
// By comparing the redirect_uri directly in the client, a first layer of protection is
// provided independently of whether the authorization server will enforce this check.
//
// See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-4.4.2.2
// for more information.
if (uri != new UriBuilder(address) { Query = null }.Uri)
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2138),
uri: SR.FormatID8000(SR.ID2138));
return default;
}
// Ensure all the query string parameters that were part of the original redirect_uri
// are present in the current request (parameters that were not part of the original
// redirect_uri are assumed to be authorization response parameters and are ignored).
if (!string.IsNullOrEmpty(address.Query) && QueryHelpers.ParseQuery(address.Query)
.Any(parameter => request.Query[parameter.Key] != parameter.Value))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2138),
uri: SR.FormatID8000(SR.ID2138));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for resolving the additional challenge parameters stored in the ASP.NET
/// Core authentication properties specified by the application that triggered the challenge operation.

99
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs

@ -11,6 +11,7 @@ using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Owin;
using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants;
using Properties = OpenIddict.Client.Owin.OpenIddictClientOwinConstants.Properties;
@ -30,6 +31,7 @@ public static partial class OpenIddictClientOwinHandlers
* Authentication processing:
*/
ValidateCorrelationCookie.Descriptor,
ValidateRedirectUri.Descriptor,
/*
* Challenge processing:
@ -301,6 +303,102 @@ public static partial class OpenIddictClientOwinHandlers
}
}
/// <summary>
/// Contains the logic responsible for comparing the current request URL to the redirect_uri stored in the state token.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class ValidateRedirectUri : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireOwinRequest>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateRedirectUri>()
.SetOrder(ValidateCorrelationCookie.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// This handler only applies to OWIN requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetOwinRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
// Try to resolve the original redirect_uri from the state token. If it cannot be resolved,
// this likely means the authorization request was sent without a redirect_uri attached.
if (!Uri.TryCreate(context.StateTokenPrincipal.GetClaim(Claims.Private.RedirectUri),
UriKind.Absolute, out Uri? address))
{
return default;
}
// Compute the absolute URL of the current request without the query string.
var uri = new Uri(request.Scheme + Uri.SchemeDelimiter + request.Host +
request.PathBase + request.Path, UriKind.Absolute);
// Compare the current HTTP request address to the original redirect_uri. If the two don't
// match, this may indicate a mix-up attack. While the authorization server is expected to
// abort the authorization flow by rejecting the token request that may be eventually sent
// with the original redirect_uri, many servers are known to incorrectly implement this
// redirect_uri validation logic. This check also offers limited protection as it cannot
// prevent the client credentials from being leaked to a malicious authorization server.
// By comparing the redirect_uri directly in the client, a first layer of protection is
// provided independently of whether the authorization server will enforce this check.
//
// See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-4.4.2.2
// for more information.
if (uri != new UriBuilder(address) { Query = null }.Uri)
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2138),
uri: SR.FormatID8000(SR.ID2138));
return default;
}
// Ensure all the query string parameters that were part of the original redirect_uri
// are present in the current request (parameters that were not part of the original
// redirect_uri are assumed to be authorization response parameters and are ignored).
if (!string.IsNullOrEmpty(address.Query) && ParseQuery(address.Query)
.Any(parameter => request.Query[parameter.Key] != parameter.Value))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2138),
uri: SR.FormatID8000(SR.ID2138));
return default;
}
return default;
static IReadOnlyDictionary<string, StringValues> ParseQuery(string query) =>
query.TrimStart(Separators.QuestionMark[0])
.Split(new[] { Separators.Ampersand[0], Separators.Semicolon[0] }, StringSplitOptions.RemoveEmptyEntries)
.Select(parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries))
.Select(parts => (
Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null,
Value: parts.Length > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
// Note: ignore empty values to match the logic used by OWIN for IOwinRequest.Query.
.Where(pair => !string.IsNullOrEmpty(pair.Key) && !string.IsNullOrEmpty(pair.Value))
.GroupBy(pair => pair.Key)
.ToDictionary(pair => pair.Key, pair => new StringValues(pair.Select(parts => parts.Value).ToArray()));
}
}
/// <summary>
/// Contains the logic responsible for resolving the additional challenge parameters stored in the ASP.NET
/// Core authentication properties specified by the application that triggered the challenge operation.
@ -430,7 +528,6 @@ public static partial class OpenIddictClientOwinHandlers
// Use the expiration date of the state token principal
// as the expiration date of the correlation cookie.
Expires = context.StateTokenPrincipal.GetExpirationDate()?.UtcDateTime
});
return default;

Loading…
Cancel
Save