From 69cd85e5522fb2fedad2987ee5b6c129347475a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Tue, 5 Jul 2022 16:31:21 +0200 Subject: [PATCH] Make the "rfp" validation logic mandatory and move common helpers to OpenIddictHelpers --- .../OpenIddict.Sandbox.AspNet.Client.csproj | 1 - .../OpenIddict.Sandbox.AspNet.Server.csproj | 1 - .../Helpers/OpenIddictHelpers.cs | 75 ++++++++++++++++++- .../OpenIddictResources.resx | 3 + .../OpenIddict.Client.AspNetCore.csproj | 4 + .../OpenIddictClientAspNetCoreHandler.cs | 43 +---------- .../OpenIddictClientAspNetCoreHandlers.cs | 4 +- .../OpenIddict.Client.Owin.csproj | 4 + .../OpenIddictClientOwinHandler.cs | 43 +---------- .../OpenIddictClientOwinHandlers.cs | 4 +- .../OpenIddict.Client.csproj | 4 + .../OpenIddictClientService.cs | 46 +----------- .../Managers/OpenIddictApplicationManager.cs | 15 ++-- .../OpenIddict.Server.csproj | 4 + ...OpenIddictServerHandlers.Authentication.cs | 23 +++--- 15 files changed, 123 insertions(+), 151 deletions(-) diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj b/sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj index 47d1c57f..11172833 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj @@ -27,7 +27,6 @@ - diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/OpenIddict.Sandbox.AspNet.Server.csproj b/sandbox/OpenIddict.Sandbox.AspNet.Server/OpenIddict.Sandbox.AspNet.Server.csproj index 8999f06c..c8011677 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/OpenIddict.Sandbox.AspNet.Server.csproj +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/OpenIddict.Sandbox.AspNet.Server.csproj @@ -31,7 +31,6 @@ - diff --git a/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs b/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs index 677a4d75..56122787 100644 --- a/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs +++ b/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs @@ -1,4 +1,8 @@ -namespace OpenIddict.Extensions; +using System.Security.Claims; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.Tokens; + +namespace OpenIddict.Extensions; /// /// Exposes common helpers used by the OpenIddict assemblies. @@ -69,4 +73,73 @@ internal static class OpenIddictHelpers } } } + + /// + /// Extracts the parameters from the specified query string. + /// + /// The query string, which may start with a '?'. + /// The parameters extracted from the specified query string. + /// is . + public static IReadOnlyDictionary ParseQuery(string query) + { + if (query is null) + { + throw new ArgumentNullException(nameof(query)); + } + + return 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())); + } + + /// + /// Creates a merged principal based on the specified principals. + /// + /// The collection of principals to merge. + /// The merged principal. + public static ClaimsPrincipal CreateMergedPrincipal(params ClaimsPrincipal?[] principals) + { + // Note: components like the client handler can be used as a pure OAuth 2.0 stack for + // delegation scenarios where the identity of the user is not needed. In this case, + // since no principal can be resolved from a token or a userinfo response to construct + // a user identity, a fake one containing an "unauthenticated" identity (i.e with its + // AuthenticationType property deliberately left to null) is used to allow the host + // to return a "successful" authentication result for these delegation-only scenarios. + if (!principals.Any(principal => principal?.Identity is ClaimsIdentity { IsAuthenticated: true })) + { + return new ClaimsPrincipal(new ClaimsIdentity()); + } + + // Create a new composite identity containing the claims of all the principals. + var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType); + + foreach (var principal in principals) + { + // Note: the principal may be null if no value was extracted from the corresponding token. + if (principal is null) + { + continue; + } + + foreach (var claim in principal.Claims) + { + // If a claim with the same type and the same value already exist, skip it. + if (identity.HasClaim(claim.Type, claim.Value)) + { + continue; + } + + identity.AddClaim(claim); + } + } + + return new ClaimsPrincipal(identity); + } } diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 9794dd4d..e6dbccb6 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1313,6 +1313,9 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad A password must be specified when using the resource owner password credentials grant. + + The request forgery protection claim cannot be resolved from the state token. + The security token is missing. diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddict.Client.AspNetCore.csproj b/src/OpenIddict.Client.AspNetCore/OpenIddict.Client.AspNetCore.csproj index ea75375d..8b3bed31 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddict.Client.AspNetCore.csproj +++ b/src/OpenIddict.Client.AspNetCore/OpenIddict.Client.AspNetCore.csproj @@ -27,6 +27,10 @@ + + + + diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs index bd7fc2ca..e4c72ccc 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs @@ -9,7 +9,7 @@ using System.Text.Encodings.Web; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; +using OpenIddict.Extensions; using static OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants; using Properties = OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants.Properties; @@ -145,7 +145,7 @@ public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler CreatePrincipal( + OpenIddictClientEndpointType.Redirection => OpenIddictHelpers.CreateMergedPrincipal( context.FrontchannelIdentityTokenPrincipal, context.BackchannelIdentityTokenPrincipal, context.UserinfoTokenPrincipal), @@ -304,45 +304,6 @@ public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler principal?.Identity is ClaimsIdentity { IsAuthenticated: true })) - { - return new ClaimsPrincipal(new ClaimsIdentity()); - } - - // Create a new composite identity containing the claims of all the principals. - var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType); - - foreach (var principal in principals) - { - // Note: the principal may be null if no value was extracted from the corresponding token. - if (principal is null) - { - continue; - } - - foreach (var claim in principal.Claims) - { - // If a claim with the same type and the same value already exist, skip it. - if (identity.HasClaim(claim.Type, claim.Value)) - { - continue; - } - - identity.AddClaim(claim); - } - } - - return new ClaimsPrincipal(identity); - } - static AuthenticationProperties CreateProperties(ClaimsPrincipal? principal) { // Note: the principal may be null if no value was extracted from the corresponding token. diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs index a88d741d..b9ea1d04 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs @@ -254,12 +254,10 @@ public static partial class OpenIddictClientAspNetCoreHandlers throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); // Resolve the request forgery protection from the state token principal. - // If the claim cannot be found, this means the protection was disabled - // using a custom event handler. In this case, bypass the validation. var identifier = context.StateTokenPrincipal.GetClaim(Claims.RequestForgeryProtection); if (string.IsNullOrEmpty(identifier)) { - return default; + throw new InvalidOperationException(SR.GetResourceString(SR.ID0339)); } // Resolve the cookie builder from the OWIN integration options. diff --git a/src/OpenIddict.Client.Owin/OpenIddict.Client.Owin.csproj b/src/OpenIddict.Client.Owin/OpenIddict.Client.Owin.csproj index 2ded9ea0..675e9366 100644 --- a/src/OpenIddict.Client.Owin/OpenIddict.Client.Owin.csproj +++ b/src/OpenIddict.Client.Owin/OpenIddict.Client.Owin.csproj @@ -19,6 +19,10 @@ + + + + diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs index f372c5f4..2d01fd37 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs @@ -6,8 +6,8 @@ using System.Security.Claims; using System.Text.Json; -using Microsoft.IdentityModel.Tokens; using Microsoft.Owin.Security.Infrastructure; +using OpenIddict.Extensions; using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants; using Properties = OpenIddict.Client.Owin.OpenIddictClientOwinConstants.Properties; @@ -161,7 +161,7 @@ public class OpenIddictClientOwinHandler : AuthenticationHandler CreatePrincipal( + OpenIddictClientEndpointType.Redirection => OpenIddictHelpers.CreateMergedPrincipal( context.FrontchannelIdentityTokenPrincipal, context.BackchannelIdentityTokenPrincipal, context.UserinfoTokenPrincipal), @@ -232,45 +232,6 @@ public class OpenIddictClientOwinHandler : AuthenticationHandler principal?.Identity is ClaimsIdentity { IsAuthenticated: true })) - { - return new ClaimsPrincipal(new ClaimsIdentity()); - } - - // Create a new composite identity containing the claims of all the principals. - var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType); - - foreach (var principal in principals) - { - // Note: the principal may be null if no value was extracted from the corresponding token. - if (principal is null) - { - continue; - } - - foreach (var claim in principal.Claims) - { - // If a claim with the same type and the same value already exist, skip it. - if (identity.HasClaim(claim.Type, claim.Value)) - { - continue; - } - - identity.AddClaim(claim); - } - } - - return new ClaimsPrincipal(identity); - } - static AuthenticationProperties CreateProperties(ClaimsPrincipal? principal) { // Note: the principal may be null if no value was extracted from the corresponding token. diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs index 8c284fc3..1b912ef8 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs @@ -253,12 +253,10 @@ public static partial class OpenIddictClientOwinHandlers throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); // Resolve the request forgery protection from the state token principal. - // If the claim cannot be found, this means the protection was disabled - // using a custom event handler. In this case, bypass the validation. var identifier = context.StateTokenPrincipal.GetClaim(Claims.RequestForgeryProtection); if (string.IsNullOrEmpty(identifier)) { - return default; + throw new InvalidOperationException(SR.GetResourceString(SR.ID0339)); } // Resolve the cookie manager and the cookie options from the OWIN integration options. diff --git a/src/OpenIddict.Client/OpenIddict.Client.csproj b/src/OpenIddict.Client/OpenIddict.Client.csproj index 9053a55d..02ba4e48 100644 --- a/src/OpenIddict.Client/OpenIddict.Client.csproj +++ b/src/OpenIddict.Client/OpenIddict.Client.csproj @@ -29,6 +29,10 @@ To use the client feature on ASP.NET Core or OWIN/Katana, reference the OpenIddi + + + + diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs index 57180da9..fac61c57 100644 --- a/src/OpenIddict.Client/OpenIddictClientService.cs +++ b/src/OpenIddict.Client/OpenIddictClientService.cs @@ -9,6 +9,7 @@ using System.Security.Claims; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; +using OpenIddict.Extensions; namespace OpenIddict.Client; @@ -399,7 +400,7 @@ public class OpenIddictClientService // Create a composite principal containing claims resolved from the // backchannel identity token and the userinfo token, if available. - return (context.TokenResponse, CreatePrincipal( + return (context.TokenResponse, OpenIddictHelpers.CreateMergedPrincipal( context.BackchannelIdentityTokenPrincipal, context.UserinfoTokenPrincipal)); } @@ -491,7 +492,7 @@ public class OpenIddictClientService // Create a composite principal containing claims resolved from the // backchannel identity token and the userinfo token, if available. - return (context.TokenResponse, CreatePrincipal( + return (context.TokenResponse, OpenIddictHelpers.CreateMergedPrincipal( context.BackchannelIdentityTokenPrincipal, context.UserinfoTokenPrincipal)); } @@ -576,7 +577,7 @@ public class OpenIddictClientService // Create a composite principal containing claims resolved from the // backchannel identity token and the userinfo token, if available. - return (context.TokenResponse, CreatePrincipal( + return (context.TokenResponse, OpenIddictHelpers.CreateMergedPrincipal( context.BackchannelIdentityTokenPrincipal, context.UserinfoTokenPrincipal)); } @@ -937,43 +938,4 @@ public class OpenIddictClientService } } } - - private static ClaimsPrincipal CreatePrincipal(params ClaimsPrincipal?[] principals) - { - // Note: the OpenIddict client handler can be used as a pure OAuth 2.0-only stack for - // delegation scenarios where the identity of the user is not needed. In this case, - // since no principal can be resolved from a token or a userinfo response to construct - // a user identity, a fake one containing an "unauthenticated" identity (i.e with its - // AuthenticationType property deliberately left to null) is used to allow ASP.NET Core - // to return a "successful" authentication result for these delegation-only scenarios. - if (!principals.Any(principal => principal?.Identity is ClaimsIdentity { IsAuthenticated: true })) - { - return new ClaimsPrincipal(new ClaimsIdentity()); - } - - // Create a new composite identity containing the claims of all the principals. - var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType); - - foreach (var principal in principals) - { - // Note: the principal may be null if no value was extracted from the corresponding token. - if (principal is null) - { - continue; - } - - foreach (var claim in principal.Claims) - { - // If a claim with the same type and the same value already exist, skip it. - if (identity.HasClaim(claim.Type, claim.Value)) - { - continue; - } - - identity.AddClaim(claim); - } - } - - return new ClaimsPrincipal(identity); - } } diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index 811aed59..344452b2 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -15,6 +15,7 @@ using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using OpenIddict.Extensions; #if !SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM using Org.BouncyCastle.Crypto; @@ -1226,15 +1227,15 @@ public class OpenIddictApplicationManager : IOpenIddictApplication // To prevent issuer fixation attacks where a malicious client would specify an "iss" parameter // in the callback URL, ensure the query - if present - doesn't include an "iss" parameter. - if (!string.IsNullOrEmpty(uri.Query) && uri.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 => parts[0] is string value ? Uri.UnescapeDataString(value) : null) - .Contains(Parameters.Iss, StringComparer.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(uri.Query)) { - yield return new ValidationResult(SR.FormatID2134(Parameters.Iss)); + var parameters = OpenIddictHelpers.ParseQuery(uri.Query); + if (parameters.ContainsKey(Parameters.Iss)) + { + yield return new ValidationResult(SR.FormatID2134(Parameters.Iss)); - break; + break; + } } } } diff --git a/src/OpenIddict.Server/OpenIddict.Server.csproj b/src/OpenIddict.Server/OpenIddict.Server.csproj index 91e739de..1ca371a6 100644 --- a/src/OpenIddict.Server/OpenIddict.Server.csproj +++ b/src/OpenIddict.Server/OpenIddict.Server.csproj @@ -27,6 +27,10 @@ To use the server feature on ASP.NET Core or OWIN/Katana, reference the OpenIddi + + + + diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index ac794dc4..f68fe969 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using OpenIddict.Extensions; namespace OpenIddict.Server; @@ -538,20 +539,20 @@ public static partial class OpenIddictServerHandlers // // Note: while OAuth 2.0 parameters are case-sentitive, the following check deliberately // uses a case-insensitive comparison to ensure that all variations of "iss" are rejected. - if (!string.IsNullOrEmpty(uri.Query) && uri.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 => parts[0] is string value ? Uri.UnescapeDataString(value) : null) - .Contains(Parameters.Iss, StringComparer.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(uri.Query)) { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6181), Parameters.RedirectUri, Parameters.Iss); + var parameters = OpenIddictHelpers.ParseQuery(uri.Query); + if (parameters.ContainsKey(Parameters.Iss)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6181), Parameters.RedirectUri, Parameters.Iss); - context.Reject( - error: Errors.InvalidRequest, - description: SR.FormatID2135(Parameters.RedirectUri, Parameters.Iss), - uri: SR.FormatID8000(SR.ID2135)); + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2135(Parameters.RedirectUri, Parameters.Iss), + uri: SR.FormatID8000(SR.ID2135)); - return default; + return default; + } } return default;