diff --git a/src/Antiforgery/.gitignore b/src/Antiforgery/.gitignore new file mode 100644 index 0000000000..6da3c6a3e9 --- /dev/null +++ b/src/Antiforgery/.gitignore @@ -0,0 +1,40 @@ +[Oo]bj/ +[Bb]in/ +TestResults/ +.nuget/ +*.sln.ide/ +_ReSharper.*/ +packages/ +artifacts/ +PublishProfiles/ +.vs/ +bower_components/ +node_modules/ +**/wwwroot/lib/ +debugSettings.json +project.lock.json +*.user +*.suo +*.cache +*.docstates +_ReSharper.* +nuget.exe +*net45.csproj +*net451.csproj +*k10.csproj +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution +*.*sdf +*.ipch +.settings +*.sln.ide +node_modules +**/[Cc]ompiler/[Rr]esources/**/*.js +*launchSettings.json +.build/ +.testPublish/ +global.json diff --git a/src/Antiforgery/Antiforgery.sln b/src/Antiforgery/Antiforgery.sln new file mode 100644 index 0000000000..a4f13419cd --- /dev/null +++ b/src/Antiforgery/Antiforgery.sln @@ -0,0 +1,35 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26208.0 +MinimumVisualStudioVersion = 15.0.26730.03 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{71D070C4-B325-48F7-9F25-DD4E91C2BBCA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6EDD8B57-4DE8-4246-A6A3-47ECD92740B4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Antiforgery", "src\Microsoft.AspNetCore.Antiforgery\Microsoft.AspNetCore.Antiforgery.csproj", "{46FB03FB-7A44-4106-BDDE-D6F5417544AB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Antiforgery.Test", "test\Microsoft.AspNetCore.Antiforgery.Test\Microsoft.AspNetCore.Antiforgery.Test.csproj", "{415E83F8-6002-47E4-AA8E-CD5169C06F28}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {46FB03FB-7A44-4106-BDDE-D6F5417544AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46FB03FB-7A44-4106-BDDE-D6F5417544AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46FB03FB-7A44-4106-BDDE-D6F5417544AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46FB03FB-7A44-4106-BDDE-D6F5417544AB}.Release|Any CPU.Build.0 = Release|Any CPU + {415E83F8-6002-47E4-AA8E-CD5169C06F28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {415E83F8-6002-47E4-AA8E-CD5169C06F28}.Debug|Any CPU.Build.0 = Debug|Any CPU + {415E83F8-6002-47E4-AA8E-CD5169C06F28}.Release|Any CPU.ActiveCfg = Release|Any CPU + {415E83F8-6002-47E4-AA8E-CD5169C06F28}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {46FB03FB-7A44-4106-BDDE-D6F5417544AB} = {71D070C4-B325-48F7-9F25-DD4E91C2BBCA} + {415E83F8-6002-47E4-AA8E-CD5169C06F28} = {6EDD8B57-4DE8-4246-A6A3-47ECD92740B4} + EndGlobalSection +EndGlobal diff --git a/src/Antiforgery/Directory.Build.props b/src/Antiforgery/Directory.Build.props new file mode 100644 index 0000000000..1008441f18 --- /dev/null +++ b/src/Antiforgery/Directory.Build.props @@ -0,0 +1,24 @@ + + + + + + + + + Microsoft ASP.NET Core + https://github.com/aspnet/Antiforgery + git + $(MSBuildThisFileDirectory) + $(MSBuildThisFileDirectory)build\Key.snk + true + true + true + + + + + + diff --git a/src/Antiforgery/Directory.Build.targets b/src/Antiforgery/Directory.Build.targets new file mode 100644 index 0000000000..53b3f6e1da --- /dev/null +++ b/src/Antiforgery/Directory.Build.targets @@ -0,0 +1,7 @@ + + + $(MicrosoftNETCoreApp20PackageVersion) + $(MicrosoftNETCoreApp21PackageVersion) + $(NETStandardLibrary20PackageVersion) + + diff --git a/src/Antiforgery/NuGetPackageVerifier.json b/src/Antiforgery/NuGetPackageVerifier.json new file mode 100644 index 0000000000..b153ab1515 --- /dev/null +++ b/src/Antiforgery/NuGetPackageVerifier.json @@ -0,0 +1,7 @@ +{ + "Default": { + "rules": [ + "DefaultCompositeRule" + ] + } +} \ No newline at end of file diff --git a/src/Antiforgery/README.md b/src/Antiforgery/README.md new file mode 100644 index 0000000000..692348309d --- /dev/null +++ b/src/Antiforgery/README.md @@ -0,0 +1,14 @@ +Antiforgery +=========== + +AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/17l06rulbn328v4k/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/Antiforgery/branch/dev) + +Travis: [![Travis](https://travis-ci.org/aspnet/Antiforgery.svg?branch=dev)](https://travis-ci.org/aspnet/Antiforgery) + +Antiforgery system for generating secure tokens to prevent Cross-Site Request Forgery attacks. + +This project is part of ASP.NET Core. You can find documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo. + +Samples can be found in [Entropy](https://github.com/aspnet/Entropy). +The [MVC](https://github.com/aspnet/Entropy/tree/dev/samples/Antiforgery.MvcWithAuthAndAjax) sample shows how to use Antiforgery in MVC when making AJAX requests. +The [Angular](https://github.com/aspnet/Entropy/tree/dev/samples/Antiforgery.Angular1) sample shows how to use Antiforgery with Angular 1. diff --git a/src/Antiforgery/build/Key.snk b/src/Antiforgery/build/Key.snk new file mode 100644 index 0000000000..e10e4889c1 Binary files /dev/null and b/src/Antiforgery/build/Key.snk differ diff --git a/src/Antiforgery/build/dependencies.props b/src/Antiforgery/build/dependencies.props new file mode 100644 index 0000000000..733502bfdc --- /dev/null +++ b/src/Antiforgery/build/dependencies.props @@ -0,0 +1,35 @@ + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + + 2.1.3-rtm-15802 + 2.0.0 + 2.1.2 + 15.6.1 + 4.7.49 + 2.0.3 + 2.3.1 + 2.4.0-beta.1.build3945 + + + + + + + + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.0 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + + \ No newline at end of file diff --git a/src/Antiforgery/build/repo.props b/src/Antiforgery/build/repo.props new file mode 100644 index 0000000000..6c9c88ab01 --- /dev/null +++ b/src/Antiforgery/build/repo.props @@ -0,0 +1,14 @@ + + + + + Internal.AspNetCore.Universe.Lineup + 2.1.0-rc1-* + https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json + + + + + + + diff --git a/src/Antiforgery/build/sources.props b/src/Antiforgery/build/sources.props new file mode 100644 index 0000000000..9215df9751 --- /dev/null +++ b/src/Antiforgery/build/sources.props @@ -0,0 +1,17 @@ + + + + + $(DotNetRestoreSources) + + $(RestoreSources); + https://dotnet.myget.org/F/dotnet-core/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; + + + $(RestoreSources); + https://api.nuget.org/v3/index.json; + + + diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryOptions.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryOptions.cs new file mode 100644 index 0000000000..e58f3f73c7 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryOptions.cs @@ -0,0 +1,150 @@ +// 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 System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Antiforgery +{ + /// + /// Provides programmatic configuration for the antiforgery token system. + /// + public class AntiforgeryOptions + { + private const string AntiforgeryTokenFieldName = "__RequestVerificationToken"; + private const string AntiforgeryTokenHeaderName = "RequestVerificationToken"; + + private string _formFieldName = AntiforgeryTokenFieldName; + + private CookieBuilder _cookieBuilder = new CookieBuilder + { + SameSite = SameSiteMode.Strict, + HttpOnly = true, + + // Check the comment on CookieBuilder for more details + IsEssential = true, + + // Some browsers do not allow non-secure endpoints to set cookies with a 'secure' flag or overwrite cookies + // whose 'secure' flag is set (http://httpwg.org/http-extensions/draft-ietf-httpbis-cookie-alone.html). + // Since mixing secure and non-secure endpoints is a common scenario in applications, we are relaxing the + // restriction on secure policy on some cookies by setting to 'None'. Cookies related to authentication or + // authorization use a stronger policy than 'None'. + SecurePolicy = CookieSecurePolicy.None, + }; + + /// + /// The default cookie prefix, which is ".AspNetCore.Antiforgery.". + /// + public static readonly string DefaultCookiePrefix = ".AspNetCore.Antiforgery."; + + /// + /// Determines the settings used to create the antiforgery cookies. + /// + /// + /// + /// If an explicit is not provided, the system will automatically generate a + /// unique name that begins with . + /// + /// + /// defaults to . + /// defaults to true. + /// defaults to true. The cookie used by the antiforgery system + /// is part of a security system that is necessary when using cookie-based authentication. It should be + /// considered required for the application to function. + /// defaults to . + /// + /// + public CookieBuilder Cookie + { + get => _cookieBuilder; + set => _cookieBuilder = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// Specifies the name of the antiforgery token field that is used by the antiforgery system. + /// + public string FormFieldName + { + get => _formFieldName; + set => _formFieldName = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// Specifies the name of the header value that is used by the antiforgery system. If null then + /// antiforgery validation will only consider form data. + /// + public string HeaderName { get; set; } = AntiforgeryTokenHeaderName; + + /// + /// Specifies whether to suppress the generation of X-Frame-Options header + /// which is used to prevent ClickJacking. By default, the X-Frame-Options + /// header is generated with the value SAMEORIGIN. If this setting is 'true', + /// the X-Frame-Options header will not be generated for the response. + /// + public bool SuppressXFrameOptionsHeader { get; set; } + + #region Obsolete API + /// + /// + /// This property is obsolete and will be removed in a future version. The recommended alternative is on . + /// + /// + /// Specifies the name of the cookie that is used by the antiforgery system. + /// + /// + /// + /// If an explicit name is not provided, the system will automatically generate a + /// unique name that begins with . + /// + [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Name) + ".")] + public string CookieName { get => Cookie.Name; set => Cookie.Name = value; } + + /// + /// + /// This property is obsolete and will be removed in a future version. The recommended alternative is on . + /// + /// + /// The path set on the cookie. If set to null, the "path" attribute on the cookie is set to the current + /// request's value. If the value of is + /// null or empty, then the "path" attribute is set to the value of . + /// + /// + [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Path) + ".")] + public PathString? CookiePath { get => Cookie.Path; set => Cookie.Path = value; } + + /// + /// + /// This property is obsolete and will be removed in a future version. The recommended alternative is on . + /// + /// + /// The domain set on the cookie. By default its null which results in the "domain" attribute not being set. + /// + /// + [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Domain) + ".")] + public string CookieDomain { get => Cookie.Domain; set => Cookie.Domain = value; } + + + /// + /// + /// This property is obsolete and will be removed in a future version. + /// The recommended alternative is to set on . + /// + /// + /// true is equivalent to . + /// false is equivalent to . + /// + /// + /// Specifies whether SSL is required for the antiforgery system + /// to operate. If this setting is 'true' and a non-SSL request + /// comes into the system, all antiforgery APIs will fail. + /// + /// + [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is to set " + nameof(Cookie) + "." + nameof(CookieBuilder.SecurePolicy) + ".")] + public bool RequireSsl + { + get => Cookie.SecurePolicy == CookieSecurePolicy.Always; + set => Cookie.SecurePolicy = value ? CookieSecurePolicy.Always : CookieSecurePolicy.None; + } + #endregion + } +} diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryServiceCollectionExtensions.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryServiceCollectionExtensions.cs new file mode 100644 index 0000000000..a82851b3e2 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryServiceCollectionExtensions.cs @@ -0,0 +1,76 @@ +// 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 System; +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Antiforgery.Internal; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extension methods for setting up antiforgery services in an . + /// + public static class AntiforgeryServiceCollectionExtensions + { + /// + /// Adds antiforgery services to the specified . + /// + /// The to add services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddAntiforgery(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddDataProtection(); + + // Don't overwrite any options setups that a user may have added. + services.TryAddEnumerable( + ServiceDescriptor.Transient, AntiforgeryOptionsSetup>()); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton>(serviceProvider => + { + var provider = serviceProvider.GetRequiredService(); + var policy = new AntiforgerySerializationContextPooledObjectPolicy(); + return provider.Create(policy); + }); + + return services; + } + + /// + /// Adds antiforgery services to the specified . + /// + /// The to add services to. + /// An to configure the provided . + /// The so that additional calls can be chained. + public static IServiceCollection AddAntiforgery(this IServiceCollection services, Action setupAction) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (setupAction == null) + { + throw new ArgumentNullException(nameof(setupAction)); + } + + services.AddAntiforgery(); + services.Configure(setupAction); + return services; + } + } +} diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryTokenSet.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryTokenSet.cs new file mode 100644 index 0000000000..033e5e0731 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryTokenSet.cs @@ -0,0 +1,57 @@ +// 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 System; + +namespace Microsoft.AspNetCore.Antiforgery +{ + /// + /// The antiforgery token pair (cookie and request token) for a request. + /// + public class AntiforgeryTokenSet + { + /// + /// Creates the antiforgery token pair (cookie and request token) for a request. + /// + /// The token that is supplied in the request. + /// The token that is supplied in the request cookie. + /// The name of the form field used for the request token. + /// The name of the header used for the request token. + public AntiforgeryTokenSet( + string requestToken, + string cookieToken, + string formFieldName, + string headerName) + { + if (formFieldName == null) + { + throw new ArgumentNullException(nameof(formFieldName)); + } + + RequestToken = requestToken; + CookieToken = cookieToken; + FormFieldName = formFieldName; + HeaderName = headerName; + } + + /// + /// Gets the request token. + /// + public string RequestToken { get; } + + /// + /// Gets the name of the form field used for the request token. + /// + public string FormFieldName { get; } + + /// + /// Gets the name of the header used for the request token. + /// + public string HeaderName { get; } + + /// + /// Gets the cookie token. + /// + public string CookieToken { get; } + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryValidationException.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryValidationException.cs new file mode 100644 index 0000000000..f1ade05d34 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryValidationException.cs @@ -0,0 +1,34 @@ +// 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 System; + +namespace Microsoft.AspNetCore.Antiforgery +{ + /// + /// The that is thrown when the antiforgery token validation fails. + /// + public class AntiforgeryValidationException : Exception + { + /// + /// Creates a new instance of with the specified + /// exception message. + /// + /// The message that describes the error. + public AntiforgeryValidationException(string message) + : base(message) + { + } + + /// + /// Creates a new instance of with the specified + /// exception message and inner exception. + /// + /// The message that describes the error. + /// The inner . + public AntiforgeryValidationException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/IAntiforgery.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/IAntiforgery.cs new file mode 100644 index 0000000000..89630a46d0 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/IAntiforgery.cs @@ -0,0 +1,65 @@ +// 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 System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Antiforgery +{ + /// + /// Provides access to the antiforgery system, which provides protection against + /// Cross-site Request Forgery (XSRF, also called CSRF) attacks. + /// + public interface IAntiforgery + { + /// + /// Generates an for this request and stores the cookie token + /// in the response. This operation also sets the "Cache-control" and "Pragma" headers to "no-cache" and + /// the "X-Frame-Options" header to "SAMEORIGIN". + /// + /// The associated with the current request. + /// An with tokens for the response. + /// + /// This method has a side effect: + /// A response cookie is set if there is no valid cookie associated with the request. + /// + AntiforgeryTokenSet GetAndStoreTokens(HttpContext httpContext); + + /// + /// Generates an for this request. + /// + /// The associated with the current request. + /// + /// Unlike , this method has no side effect. The caller + /// is responsible for setting the response cookie and injecting the returned + /// form token as appropriate. + /// + AntiforgeryTokenSet GetTokens(HttpContext httpContext); + + /// + /// Asynchronously returns a value indicating whether the request passes antiforgery validation. If the + /// request uses a safe HTTP method (GET, HEAD, OPTIONS, TRACE), the antiforgery token is not validated. + /// + /// The associated with the current request. + /// + /// A that, when completed, returns true if the request uses a safe HTTP + /// method or contains a valid antiforgery token, otherwise returns false. + /// + Task IsRequestValidAsync(HttpContext httpContext); + + /// + /// Validates an antiforgery token that was supplied as part of the request. + /// + /// The associated with the current request. + /// + /// Thrown when the request does not include a valid antiforgery token. + /// + Task ValidateRequestAsync(HttpContext httpContext); + + /// + /// Generates and stores an antiforgery cookie token if one is not available or not valid. + /// + /// The associated with the current request. + void SetCookieTokenAndHeader(HttpContext httpContext); + } +} diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/IAntiforgeryAdditionalDataProvider.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/IAntiforgeryAdditionalDataProvider.cs new file mode 100644 index 0000000000..d66b6245db --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/IAntiforgeryAdditionalDataProvider.cs @@ -0,0 +1,39 @@ +// 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.Http; + +namespace Microsoft.AspNetCore.Antiforgery +{ + /// + /// Allows providing or validating additional custom data for antiforgery tokens. + /// For example, the developer could use this to supply a nonce when the token is + /// generated, then he could validate the nonce when the token is validated. + /// + /// + /// The antiforgery system already embeds the client's username within the + /// generated tokens. This interface provides and consumes supplemental + /// data. If an incoming antiforgery token contains supplemental data but no + /// additional data provider is configured, the supplemental data will not be + /// validated. + /// + public interface IAntiforgeryAdditionalDataProvider + { + /// + /// Provides additional data to be stored for the antiforgery tokens generated + /// during this request. + /// + /// Information about the current request. + /// Supplemental data to embed within the antiforgery token. + string GetAdditionalData(HttpContext context); + + /// + /// Validates additional data that was embedded inside an incoming antiforgery + /// token. + /// + /// Information about the current request. + /// Supplemental data that was embedded within the token. + /// True if the data is valid; false if the data is invalid. + bool ValidateAdditionalData(HttpContext context, string additionalData); + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryFeature.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryFeature.cs new file mode 100644 index 0000000000..ad2a38501d --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryFeature.cs @@ -0,0 +1,34 @@ +// 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. + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + /// + /// Used to hold per-request state. + /// + public class AntiforgeryFeature : IAntiforgeryFeature + { + public bool HaveDeserializedCookieToken { get; set; } + + public AntiforgeryToken CookieToken { get; set; } + + public bool HaveDeserializedRequestToken { get; set; } + + public AntiforgeryToken RequestToken { get; set; } + + public bool HaveGeneratedNewCookieToken { get; set; } + + // After HaveGeneratedNewCookieToken is true, remains null if CookieToken is valid. + public AntiforgeryToken NewCookieToken { get; set; } + + // After HaveGeneratedNewCookieToken is true, remains null if CookieToken is valid. + public string NewCookieTokenString { get; set; } + + public AntiforgeryToken NewRequestToken { get; set; } + + public string NewRequestTokenString { get; set; } + + // Always false if NewCookieToken is null. Never store null cookie token or re-store cookie token from request. + public bool HaveStoredNewCookieToken { get; set; } + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryLoggerExtensions.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryLoggerExtensions.cs new file mode 100644 index 0000000000..232279e4be --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryLoggerExtensions.cs @@ -0,0 +1,109 @@ +// 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 System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + internal static class AntiforgeryLoggerExtensions + { + private static readonly Action _failedToDeserialzeTokens; + private static readonly Action _validationFailed; + private static readonly Action _validated; + private static readonly Action _missingCookieToken; + private static readonly Action _missingRequestToken; + private static readonly Action _newCookieToken; + private static readonly Action _reusedCookieToken; + private static readonly Action _tokenDeserializeException; + private static readonly Action _responseCacheHeadersOverridenToNoCache; + + static AntiforgeryLoggerExtensions() + { + _validationFailed = LoggerMessage.Define( + LogLevel.Warning, + 1, + "Antiforgery validation failed with message '{Message}'."); + _validated = LoggerMessage.Define( + LogLevel.Debug, + 2, + "Antiforgery successfully validated a request."); + _missingCookieToken = LoggerMessage.Define( + LogLevel.Warning, + 3, + "The required antiforgery cookie '{CookieName}' is not present."); + _missingRequestToken = LoggerMessage.Define( + LogLevel.Warning, + 4, + "The required antiforgery request token was not provided in either form field '{FormFieldName}' " + + "or header '{HeaderName}'."); + _newCookieToken = LoggerMessage.Define( + LogLevel.Debug, + 5, + "A new antiforgery cookie token was created."); + _reusedCookieToken = LoggerMessage.Define( + LogLevel.Debug, + 6, + "An antiforgery cookie token was reused."); + _tokenDeserializeException = LoggerMessage.Define( + LogLevel.Error, + 7, + "An exception was thrown while deserializing the token."); + _responseCacheHeadersOverridenToNoCache = LoggerMessage.Define( + LogLevel.Warning, + 8, + "The 'Cache-Control' and 'Pragma' headers have been overridden and set to 'no-cache, no-store' and " + + "'no-cache' respectively to prevent caching of this response. Any response that uses antiforgery " + + "should not be cached."); + _failedToDeserialzeTokens = LoggerMessage.Define( + LogLevel.Debug, + 9, + "Failed to deserialize antiforgery tokens."); + } + + public static void ValidationFailed(this ILogger logger, string message) + { + _validationFailed(logger, message, null); + } + + public static void ValidatedAntiforgeryToken(this ILogger logger) + { + _validated(logger, null); + } + + public static void MissingCookieToken(this ILogger logger, string cookieName) + { + _missingCookieToken(logger, cookieName, null); + } + + public static void MissingRequestToken(this ILogger logger, string formFieldName, string headerName) + { + _missingRequestToken(logger, formFieldName, headerName, null); + } + + public static void NewCookieToken(this ILogger logger) + { + _newCookieToken(logger, null); + } + + public static void ReusedCookieToken(this ILogger logger) + { + _reusedCookieToken(logger, null); + } + + public static void TokenDeserializeException(this ILogger logger, Exception exception) + { + _tokenDeserializeException(logger, exception); + } + + public static void ResponseCacheHeadersOverridenToNoCache(this ILogger logger) + { + _responseCacheHeadersOverridenToNoCache(logger, null); + } + + public static void FailedToDeserialzeTokens(this ILogger logger, Exception exception) + { + _failedToDeserialzeTokens(logger, exception); + } + } +} diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryOptionsSetup.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryOptionsSetup.cs new file mode 100644 index 0000000000..a6bc826351 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryOptionsSetup.cs @@ -0,0 +1,38 @@ +// 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 System.Linq; +using System.Text; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class AntiforgeryOptionsSetup : ConfigureOptions + { + public AntiforgeryOptionsSetup(IOptions dataProtectionOptionsAccessor) + : base((options) => ConfigureOptions(options, dataProtectionOptionsAccessor.Value)) + { + } + + public static void ConfigureOptions(AntiforgeryOptions options, DataProtectionOptions dataProtectionOptions) + { + if (options.Cookie.Name == null) + { + var applicationId = dataProtectionOptions.ApplicationDiscriminator ?? string.Empty; + options.Cookie.Name = AntiforgeryOptions.DefaultCookiePrefix + ComputeCookieName(applicationId); + } + } + + private static string ComputeCookieName(string applicationId) + { + using (var sha256 = CryptographyAlgorithms.CreateSHA256()) + { + var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(applicationId)); + var subHash = hash.Take(8).ToArray(); + return WebEncoders.Base64UrlEncode(subHash); + } + } + } +} diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgerySerializationContext.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgerySerializationContext.cs new file mode 100644 index 0000000000..6d697fa0da --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgerySerializationContext.cs @@ -0,0 +1,141 @@ +// 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 System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class AntiforgerySerializationContext + { + // Avoid allocating 256 bytes (the default) and using 18 (the AntiforgeryToken minimum). 64 bytes is enough for + // a short username or claim UID and some additional data. MemoryStream bumps capacity to 256 if exceeded. + private const int InitialStreamSize = 64; + + // Don't let the MemoryStream grow beyond 1 MB. + private const int MaximumStreamSize = 0x100000; + + // Start _chars off with length 256 (18 bytes is protected into 116 bytes then encoded into 156 characters). + // Double length from there if necessary. + private const int InitialCharsLength = 256; + + // Don't let _chars grow beyond 512k characters. + private const int MaximumCharsLength = 0x80000; + + private char[] _chars; + private MemoryStream _stream; + private BinaryReader _reader; + private BinaryWriter _writer; + private SHA256 _sha256; + + public MemoryStream Stream + { + get + { + if (_stream == null) + { + _stream = new MemoryStream(InitialStreamSize); + } + + return _stream; + } + private set + { + _stream = value; + } + } + + public BinaryReader Reader + { + get + { + if (_reader == null) + { + // Leave open to clean up correctly even if only one of the reader or writer has been created. + _reader = new BinaryReader(Stream, Encoding.UTF8, leaveOpen: true); + } + + return _reader; + } + private set + { + _reader = value; + } + } + + public BinaryWriter Writer + { + get + { + if (_writer == null) + { + // Leave open to clean up correctly even if only one of the reader or writer has been created. + _writer = new BinaryWriter(Stream, Encoding.UTF8, leaveOpen: true); + } + + return _writer; + } + private set + { + _writer = value; + } + } + + public SHA256 Sha256 + { + get + { + if (_sha256 == null) + { + _sha256 = CryptographyAlgorithms.CreateSHA256(); + } + + return _sha256; + } + private set + { + _sha256 = value; + } + } + + public char[] GetChars(int count) + { + if (_chars == null || _chars.Length < count) + { + var newLength = _chars == null ? InitialCharsLength : checked(_chars.Length * 2); + while (newLength < count) + { + newLength = checked(newLength * 2); + } + + _chars = new char[newLength]; + } + + return _chars; + } + + public void Reset() + { + if (_chars != null && _chars.Length > MaximumCharsLength) + { + _chars = null; + } + + if (_stream != null) + { + if (Stream.Capacity > MaximumStreamSize) + { + Stream = null; + Reader = null; + Writer = null; + } + else + { + Stream.Position = 0L; + Stream.SetLength(0L); + } + } + } + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgerySerializationContextPooledObjectPolicy.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgerySerializationContextPooledObjectPolicy.cs new file mode 100644 index 0000000000..0a6163141b --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgerySerializationContextPooledObjectPolicy.cs @@ -0,0 +1,23 @@ +// 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.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class AntiforgerySerializationContextPooledObjectPolicy + : IPooledObjectPolicy + { + public AntiforgerySerializationContext Create() + { + return new AntiforgerySerializationContext(); + } + + public bool Return(AntiforgerySerializationContext obj) + { + obj.Reset(); + + return true; + } + } +} diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryToken.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryToken.cs new file mode 100644 index 0000000000..78294f730c --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryToken.cs @@ -0,0 +1,53 @@ +// 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. + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public sealed class AntiforgeryToken + { + internal const int SecurityTokenBitLength = 128; + internal const int ClaimUidBitLength = 256; + + private string _additionalData = string.Empty; + private string _username = string.Empty; + private BinaryBlob _securityToken; + + public string AdditionalData + { + get { return _additionalData; } + set + { + _additionalData = value ?? string.Empty; + } + } + + public BinaryBlob ClaimUid { get; set; } + + public bool IsCookieToken { get; set; } + + public BinaryBlob SecurityToken + { + get + { + if (_securityToken == null) + { + _securityToken = new BinaryBlob(SecurityTokenBitLength); + } + return _securityToken; + } + set + { + _securityToken = value; + } + } + + public string Username + { + get { return _username; } + set + { + _username = value ?? string.Empty; + } + } + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/BinaryBlob.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/BinaryBlob.cs new file mode 100644 index 0000000000..88c34571c0 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/BinaryBlob.cs @@ -0,0 +1,117 @@ +// 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 System; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + // Represents a binary blob (token) that contains random data. + // Useful for binary data inside a serialized stream. + [DebuggerDisplay("{DebuggerString}")] + public sealed class BinaryBlob : IEquatable + { + private static readonly RandomNumberGenerator _randomNumberGenerator = RandomNumberGenerator.Create(); + private readonly byte[] _data; + + // Generates a new token using a specified bit length. + public BinaryBlob(int bitLength) + : this(bitLength, GenerateNewToken(bitLength)) + { + } + + // Generates a token using an existing binary value. + public BinaryBlob(int bitLength, byte[] data) + { + if (bitLength < 32 || bitLength % 8 != 0) + { + throw new ArgumentOutOfRangeException("bitLength"); + } + if (data == null || data.Length != bitLength / 8) + { + throw new ArgumentOutOfRangeException("data"); + } + + _data = data; + } + + public int BitLength + { + get + { + return checked(_data.Length * 8); + } + } + + private string DebuggerString + { + get + { + var sb = new StringBuilder("0x", 2 + (_data.Length * 2)); + for (var i = 0; i < _data.Length; i++) + { + sb.AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", _data[i]); + } + return sb.ToString(); + } + } + + public override bool Equals(object obj) + { + return Equals(obj as BinaryBlob); + } + + public bool Equals(BinaryBlob other) + { + if (other == null) + { + return false; + } + + Debug.Assert(_data.Length == other._data.Length); + return AreByteArraysEqual(_data, other._data); + } + + public byte[] GetData() + { + return _data; + } + + public override int GetHashCode() + { + // Since data should contain uniformly-distributed entropy, the + // first 32 bits can serve as the hash code. + Debug.Assert(_data != null && _data.Length >= (32 / 8)); + return BitConverter.ToInt32(_data, 0); + } + + private static byte[] GenerateNewToken(int bitLength) + { + var data = new byte[bitLength / 8]; + _randomNumberGenerator.GetBytes(data); + return data; + } + + // Need to mark it with NoInlining and NoOptimization attributes to ensure that the + // operation runs in constant time. + [MethodImplAttribute(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private static bool AreByteArraysEqual(byte[] a, byte[] b) + { + if (a == null || b == null || a.Length != b.Length) + { + return false; + } + + var areEqual = true; + for (var i = 0; i < a.Length; i++) + { + areEqual &= (a[i] == b[i]); + } + return areEqual; + } + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/CryptographyAlgorithms.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/CryptographyAlgorithms.cs new file mode 100644 index 0000000000..644b4e6234 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/CryptographyAlgorithms.cs @@ -0,0 +1,25 @@ +// 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 System.Security.Cryptography; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public static class CryptographyAlgorithms + { + public static SHA256 CreateSHA256() + { + try + { + return SHA256.Create(); + } + // SHA256.Create is documented to throw this exception on FIPS compliant machines. + // See: https://msdn.microsoft.com/en-us/library/z08hz7ad%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396 + catch (System.Reflection.TargetInvocationException) + { + // Fallback to a FIPS compliant SHA256 algorithm. + return new SHA256CryptoServiceProvider(); + } + } + } +} diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgery.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgery.cs new file mode 100644 index 0000000000..cef46be893 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgery.cs @@ -0,0 +1,497 @@ +// 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 System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + /// + /// Provides access to the antiforgery system, which provides protection against + /// Cross-site Request Forgery (XSRF, also called CSRF) attacks. + /// + public class DefaultAntiforgery : IAntiforgery + { + private readonly AntiforgeryOptions _options; + private readonly IAntiforgeryTokenGenerator _tokenGenerator; + private readonly IAntiforgeryTokenSerializer _tokenSerializer; + private readonly IAntiforgeryTokenStore _tokenStore; + private readonly ILogger _logger; + + public DefaultAntiforgery( + IOptions antiforgeryOptionsAccessor, + IAntiforgeryTokenGenerator tokenGenerator, + IAntiforgeryTokenSerializer tokenSerializer, + IAntiforgeryTokenStore tokenStore, + ILoggerFactory loggerFactory) + { + _options = antiforgeryOptionsAccessor.Value; + _tokenGenerator = tokenGenerator; + _tokenSerializer = tokenSerializer; + _tokenStore = tokenStore; + _logger = loggerFactory.CreateLogger(); + } + + /// + public AntiforgeryTokenSet GetAndStoreTokens(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + CheckSSLConfig(httpContext); + + var antiforgeryFeature = GetTokensInternal(httpContext); + var tokenSet = Serialize(antiforgeryFeature); + + if (!antiforgeryFeature.HaveStoredNewCookieToken) + { + if (antiforgeryFeature.NewCookieToken != null) + { + // Serialize handles the new cookie token string. + Debug.Assert(antiforgeryFeature.NewCookieTokenString != null); + + SaveCookieTokenAndHeader(httpContext, antiforgeryFeature.NewCookieTokenString); + antiforgeryFeature.HaveStoredNewCookieToken = true; + _logger.NewCookieToken(); + } + else + { + _logger.ReusedCookieToken(); + } + } + + if (!httpContext.Response.HasStarted) + { + // Explicitly set the cache headers to 'no-cache'. This could override any user set value but this is fine + // as a response with antiforgery token must never be cached. + SetDoNotCacheHeaders(httpContext); + } + + return tokenSet; + } + + /// + public AntiforgeryTokenSet GetTokens(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + CheckSSLConfig(httpContext); + + var antiforgeryFeature = GetTokensInternal(httpContext); + return Serialize(antiforgeryFeature); + } + + /// + public async Task IsRequestValidAsync(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + CheckSSLConfig(httpContext); + + var method = httpContext.Request.Method; + if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) || + string.Equals(method, "HEAD", StringComparison.OrdinalIgnoreCase) || + string.Equals(method, "OPTIONS", StringComparison.OrdinalIgnoreCase) || + string.Equals(method, "TRACE", StringComparison.OrdinalIgnoreCase)) + { + // Validation not needed for these request types. + return true; + } + + var tokens = await _tokenStore.GetRequestTokensAsync(httpContext); + if (tokens.CookieToken == null) + { + _logger.MissingCookieToken(_options.Cookie.Name); + return false; + } + + if (tokens.RequestToken == null) + { + _logger.MissingRequestToken(_options.FormFieldName, _options.HeaderName); + return false; + } + + // Extract cookie & request tokens + AntiforgeryToken deserializedCookieToken; + AntiforgeryToken deserializedRequestToken; + if (!TryDeserializeTokens(httpContext, tokens, out deserializedCookieToken, out deserializedRequestToken)) + { + return false; + } + + // Validate + string message; + var result = _tokenGenerator.TryValidateTokenSet( + httpContext, + deserializedCookieToken, + deserializedRequestToken, + out message); + + if (result) + { + _logger.ValidatedAntiforgeryToken(); + } + else + { + _logger.ValidationFailed(message); + } + + return result; + } + + /// + public async Task ValidateRequestAsync(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + CheckSSLConfig(httpContext); + + var tokens = await _tokenStore.GetRequestTokensAsync(httpContext); + if (tokens.CookieToken == null) + { + throw new AntiforgeryValidationException( + Resources.FormatAntiforgery_CookieToken_MustBeProvided(_options.Cookie.Name)); + } + + if (tokens.RequestToken == null) + { + if (_options.HeaderName == null) + { + var message = Resources.FormatAntiforgery_FormToken_MustBeProvided(_options.FormFieldName); + throw new AntiforgeryValidationException(message); + } + else if (!httpContext.Request.HasFormContentType) + { + var message = Resources.FormatAntiforgery_HeaderToken_MustBeProvided(_options.HeaderName); + throw new AntiforgeryValidationException(message); + } + else + { + var message = Resources.FormatAntiforgery_RequestToken_MustBeProvided( + _options.FormFieldName, + _options.HeaderName); + throw new AntiforgeryValidationException(message); + } + } + + ValidateTokens(httpContext, tokens); + + _logger.ValidatedAntiforgeryToken(); + } + + private void ValidateTokens(HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet) + { + Debug.Assert(!string.IsNullOrEmpty(antiforgeryTokenSet.CookieToken)); + Debug.Assert(!string.IsNullOrEmpty(antiforgeryTokenSet.RequestToken)); + + // Extract cookie & request tokens + AntiforgeryToken deserializedCookieToken; + AntiforgeryToken deserializedRequestToken; + + DeserializeTokens( + httpContext, + antiforgeryTokenSet, + out deserializedCookieToken, + out deserializedRequestToken); + + // Validate + string message; + if (!_tokenGenerator.TryValidateTokenSet( + httpContext, + deserializedCookieToken, + deserializedRequestToken, + out message)) + { + throw new AntiforgeryValidationException(message); + } + } + + /// + public void SetCookieTokenAndHeader(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + CheckSSLConfig(httpContext); + + var antiforgeryFeature = GetCookieTokens(httpContext); + if (!antiforgeryFeature.HaveStoredNewCookieToken && antiforgeryFeature.NewCookieToken != null) + { + if (antiforgeryFeature.NewCookieTokenString == null) + { + antiforgeryFeature.NewCookieTokenString = + _tokenSerializer.Serialize(antiforgeryFeature.NewCookieToken); + } + + SaveCookieTokenAndHeader(httpContext, antiforgeryFeature.NewCookieTokenString); + antiforgeryFeature.HaveStoredNewCookieToken = true; + _logger.NewCookieToken(); + } + else + { + _logger.ReusedCookieToken(); + } + + if (!httpContext.Response.HasStarted) + { + SetDoNotCacheHeaders(httpContext); + } + } + + private void SaveCookieTokenAndHeader(HttpContext httpContext, string cookieToken) + { + if (cookieToken != null) + { + // Persist the new cookie if it is not null. + _tokenStore.SaveCookieToken(httpContext, cookieToken); + } + + if (!_options.SuppressXFrameOptionsHeader && !httpContext.Response.Headers.ContainsKey("X-Frame-Options")) + { + // Adding X-Frame-Options header to prevent ClickJacking. See + // http://tools.ietf.org/html/draft-ietf-websec-x-frame-options-10 + // for more information. + httpContext.Response.Headers["X-Frame-Options"] = "SAMEORIGIN"; + } + } + + private void CheckSSLConfig(HttpContext context) + { + if (_options.Cookie.SecurePolicy == CookieSecurePolicy.Always && !context.Request.IsHttps) + { + throw new InvalidOperationException(Resources.FormatAntiforgery_RequiresSSL( + string.Join(".", nameof(AntiforgeryOptions), nameof(AntiforgeryOptions.Cookie), nameof(CookieBuilder.SecurePolicy)), + nameof(CookieSecurePolicy.Always))); + } + } + + private static IAntiforgeryFeature GetAntiforgeryFeature(HttpContext httpContext) + { + var antiforgeryFeature = httpContext.Features.Get(); + if (antiforgeryFeature == null) + { + antiforgeryFeature = new AntiforgeryFeature(); + httpContext.Features.Set(antiforgeryFeature); + } + + return antiforgeryFeature; + } + + private IAntiforgeryFeature GetCookieTokens(HttpContext httpContext) + { + var antiforgeryFeature = GetAntiforgeryFeature(httpContext); + + if (antiforgeryFeature.HaveGeneratedNewCookieToken) + { + Debug.Assert(antiforgeryFeature.HaveDeserializedCookieToken); + + // Have executed this method earlier in the context of this request. + return antiforgeryFeature; + } + + AntiforgeryToken cookieToken; + if (antiforgeryFeature.HaveDeserializedCookieToken) + { + cookieToken = antiforgeryFeature.CookieToken; + } + else + { + cookieToken = GetCookieTokenDoesNotThrow(httpContext); + + antiforgeryFeature.CookieToken = cookieToken; + antiforgeryFeature.HaveDeserializedCookieToken = true; + } + + AntiforgeryToken newCookieToken; + if (_tokenGenerator.IsCookieTokenValid(cookieToken)) + { + // No need for the cookie token from the request after it has been verified. + newCookieToken = null; + } + else + { + // Need to make sure we're always operating with a good cookie token. + newCookieToken = _tokenGenerator.GenerateCookieToken(); + Debug.Assert(_tokenGenerator.IsCookieTokenValid(newCookieToken)); + } + + antiforgeryFeature.HaveGeneratedNewCookieToken = true; + antiforgeryFeature.NewCookieToken = newCookieToken; + + return antiforgeryFeature; + } + + private AntiforgeryToken GetCookieTokenDoesNotThrow(HttpContext httpContext) + { + try + { + var serializedToken = _tokenStore.GetCookieToken(httpContext); + + if (serializedToken != null) + { + var token = _tokenSerializer.Deserialize(serializedToken); + return token; + } + } + catch (Exception ex) + { + // ignore failures since we'll just generate a new token + _logger.TokenDeserializeException(ex); + } + + return null; + } + + private IAntiforgeryFeature GetTokensInternal(HttpContext httpContext) + { + var antiforgeryFeature = GetCookieTokens(httpContext); + if (antiforgeryFeature.NewRequestToken == null) + { + var cookieToken = antiforgeryFeature.NewCookieToken ?? antiforgeryFeature.CookieToken; + antiforgeryFeature.NewRequestToken = _tokenGenerator.GenerateRequestToken( + httpContext, + cookieToken); + } + + return antiforgeryFeature; + } + + /// + /// Sets the 'Cache-Control' header to 'no-cache, no-store' and 'Pragma' header to 'no-cache' overriding any user set value. + /// + /// The . + protected virtual void SetDoNotCacheHeaders(HttpContext httpContext) + { + // Since antifogery token generation is not very obvious to the end users (ex: MVC's form tag generates them + // by default), log a warning to let users know of the change in behavior to any cache headers they might + // have set explicitly. + LogCacheHeaderOverrideWarning(httpContext.Response); + + httpContext.Response.Headers[HeaderNames.CacheControl] = "no-cache, no-store"; + httpContext.Response.Headers[HeaderNames.Pragma] = "no-cache"; + } + + private void LogCacheHeaderOverrideWarning(HttpResponse response) + { + var logWarning = false; + CacheControlHeaderValue cacheControlHeaderValue; + if (CacheControlHeaderValue.TryParse(response.Headers[HeaderNames.CacheControl].ToString(), out cacheControlHeaderValue)) + { + if (!cacheControlHeaderValue.NoCache) + { + logWarning = true; + } + } + + var pragmaHeader = response.Headers[HeaderNames.Pragma]; + if (!logWarning + && !string.IsNullOrEmpty(pragmaHeader) + && string.Compare(pragmaHeader, "no-cache", ignoreCase: true) != 0) + { + logWarning = true; + } + + if (logWarning) + { + _logger.ResponseCacheHeadersOverridenToNoCache(); + } + } + + private AntiforgeryTokenSet Serialize(IAntiforgeryFeature antiforgeryFeature) + { + // Should only be called after new tokens have been generated. + Debug.Assert(antiforgeryFeature.HaveGeneratedNewCookieToken); + Debug.Assert(antiforgeryFeature.NewRequestToken != null); + + if (antiforgeryFeature.NewRequestTokenString == null) + { + antiforgeryFeature.NewRequestTokenString = + _tokenSerializer.Serialize(antiforgeryFeature.NewRequestToken); + } + + if (antiforgeryFeature.NewCookieTokenString == null && antiforgeryFeature.NewCookieToken != null) + { + antiforgeryFeature.NewCookieTokenString = + _tokenSerializer.Serialize(antiforgeryFeature.NewCookieToken); + } + + return new AntiforgeryTokenSet( + antiforgeryFeature.NewRequestTokenString, + antiforgeryFeature.NewCookieTokenString, + _options.FormFieldName, + _options.HeaderName); + } + + private bool TryDeserializeTokens( + HttpContext httpContext, + AntiforgeryTokenSet antiforgeryTokenSet, + out AntiforgeryToken cookieToken, + out AntiforgeryToken requestToken) + { + try + { + DeserializeTokens(httpContext, antiforgeryTokenSet, out cookieToken, out requestToken); + return true; + } + catch (AntiforgeryValidationException ex) + { + _logger.FailedToDeserialzeTokens(ex); + + cookieToken = null; + requestToken = null; + return false; + } + } + + private void DeserializeTokens( + HttpContext httpContext, + AntiforgeryTokenSet antiforgeryTokenSet, + out AntiforgeryToken cookieToken, + out AntiforgeryToken requestToken) + { + var antiforgeryFeature = GetAntiforgeryFeature(httpContext); + + if (antiforgeryFeature.HaveDeserializedCookieToken) + { + cookieToken = antiforgeryFeature.CookieToken; + } + else + { + cookieToken = _tokenSerializer.Deserialize(antiforgeryTokenSet.CookieToken); + + antiforgeryFeature.CookieToken = cookieToken; + antiforgeryFeature.HaveDeserializedCookieToken = true; + } + + if (antiforgeryFeature.HaveDeserializedRequestToken) + { + requestToken = antiforgeryFeature.RequestToken; + } + else + { + requestToken = _tokenSerializer.Deserialize(antiforgeryTokenSet.RequestToken); + + antiforgeryFeature.RequestToken = requestToken; + antiforgeryFeature.HaveDeserializedRequestToken = true; + } + } + } +} diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryAdditionalDataProvider.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryAdditionalDataProvider.cs new file mode 100644 index 0000000000..ad28453495 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryAdditionalDataProvider.cs @@ -0,0 +1,26 @@ +// 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.Http; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + /// + /// A default implementation. + /// + public class DefaultAntiforgeryAdditionalDataProvider : IAntiforgeryAdditionalDataProvider + { + /// + public virtual string GetAdditionalData(HttpContext context) + { + return string.Empty; + } + + /// + public virtual bool ValidateAdditionalData(HttpContext context, string additionalData) + { + // Default implementation does not understand anything but empty data. + return string.IsNullOrEmpty(additionalData); + } + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenGenerator.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenGenerator.cs new file mode 100644 index 0000000000..872f7ed18c --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenGenerator.cs @@ -0,0 +1,238 @@ +// 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 System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Security.Principal; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class DefaultAntiforgeryTokenGenerator : IAntiforgeryTokenGenerator + { + private readonly IClaimUidExtractor _claimUidExtractor; + private readonly IAntiforgeryAdditionalDataProvider _additionalDataProvider; + + public DefaultAntiforgeryTokenGenerator( + IClaimUidExtractor claimUidExtractor, + IAntiforgeryAdditionalDataProvider additionalDataProvider) + { + _claimUidExtractor = claimUidExtractor; + _additionalDataProvider = additionalDataProvider; + } + + /// + public AntiforgeryToken GenerateCookieToken() + { + return new AntiforgeryToken() + { + // SecurityToken will be populated automatically. + IsCookieToken = true + }; + } + + /// + public AntiforgeryToken GenerateRequestToken( + HttpContext httpContext, + AntiforgeryToken cookieToken) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (cookieToken == null) + { + throw new ArgumentNullException(nameof(cookieToken)); + } + + if (!IsCookieTokenValid(cookieToken)) + { + throw new ArgumentException( + Resources.Antiforgery_CookieToken_IsInvalid, + nameof(cookieToken)); + } + + var requestToken = new AntiforgeryToken() + { + SecurityToken = cookieToken.SecurityToken, + IsCookieToken = false + }; + + var isIdentityAuthenticated = false; + + // populate Username and ClaimUid + var authenticatedIdentity = GetAuthenticatedIdentity(httpContext.User); + if (authenticatedIdentity != null) + { + isIdentityAuthenticated = true; + requestToken.ClaimUid = GetClaimUidBlob(_claimUidExtractor.ExtractClaimUid(httpContext.User)); + + if (requestToken.ClaimUid == null) + { + requestToken.Username = authenticatedIdentity.Name; + } + } + + // populate AdditionalData + if (_additionalDataProvider != null) + { + requestToken.AdditionalData = _additionalDataProvider.GetAdditionalData(httpContext); + } + + if (isIdentityAuthenticated + && string.IsNullOrEmpty(requestToken.Username) + && requestToken.ClaimUid == null + && string.IsNullOrEmpty(requestToken.AdditionalData)) + { + // Application says user is authenticated, but we have no identifier for the user. + throw new InvalidOperationException( + Resources.FormatAntiforgeryTokenValidator_AuthenticatedUserWithoutUsername( + authenticatedIdentity.GetType(), + nameof(IIdentity.IsAuthenticated), + "true", + nameof(IIdentity.Name), + nameof(IAntiforgeryAdditionalDataProvider), + nameof(DefaultAntiforgeryAdditionalDataProvider))); + } + + return requestToken; + } + + /// + public bool IsCookieTokenValid(AntiforgeryToken cookieToken) + { + return cookieToken != null && cookieToken.IsCookieToken; + } + + /// + public bool TryValidateTokenSet( + HttpContext httpContext, + AntiforgeryToken cookieToken, + AntiforgeryToken requestToken, + out string message) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (cookieToken == null) + { + throw new ArgumentNullException( + nameof(cookieToken), + Resources.Antiforgery_CookieToken_MustBeProvided_Generic); + } + + if (requestToken == null) + { + throw new ArgumentNullException( + nameof(requestToken), + Resources.Antiforgery_RequestToken_MustBeProvided_Generic); + } + + // Do the tokens have the correct format? + if (!cookieToken.IsCookieToken || requestToken.IsCookieToken) + { + message = Resources.AntiforgeryToken_TokensSwapped; + return false; + } + + // Are the security tokens embedded in each incoming token identical? + if (!object.Equals(cookieToken.SecurityToken, requestToken.SecurityToken)) + { + message = Resources.AntiforgeryToken_SecurityTokenMismatch; + return false; + } + + // Is the incoming token meant for the current user? + var currentUsername = string.Empty; + BinaryBlob currentClaimUid = null; + + var authenticatedIdentity = GetAuthenticatedIdentity(httpContext.User); + if (authenticatedIdentity != null) + { + currentClaimUid = GetClaimUidBlob(_claimUidExtractor.ExtractClaimUid(httpContext.User)); + if (currentClaimUid == null) + { + currentUsername = authenticatedIdentity.Name ?? string.Empty; + } + } + + // OpenID and other similar authentication schemes use URIs for the username. + // These should be treated as case-sensitive. + var comparer = StringComparer.OrdinalIgnoreCase; + if (currentUsername.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + currentUsername.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + comparer = StringComparer.Ordinal; + } + + if (!comparer.Equals(requestToken.Username, currentUsername)) + { + message = Resources.FormatAntiforgeryToken_UsernameMismatch(requestToken.Username, currentUsername); + return false; + } + + if (!object.Equals(requestToken.ClaimUid, currentClaimUid)) + { + message = Resources.AntiforgeryToken_ClaimUidMismatch; + return false; + } + + // Is the AdditionalData valid? + if (_additionalDataProvider != null && + !_additionalDataProvider.ValidateAdditionalData(httpContext, requestToken.AdditionalData)) + { + message = Resources.AntiforgeryToken_AdditionalDataCheckFailed; + return false; + } + + message = null; + return true; + } + + private static BinaryBlob GetClaimUidBlob(string base64ClaimUid) + { + if (base64ClaimUid == null) + { + return null; + } + + return new BinaryBlob(256, Convert.FromBase64String(base64ClaimUid)); + } + + private static ClaimsIdentity GetAuthenticatedIdentity(ClaimsPrincipal claimsPrincipal) + { + if (claimsPrincipal == null) + { + return null; + } + + var identitiesList = claimsPrincipal.Identities as List; + if (identitiesList != null) + { + for (var i = 0; i < identitiesList.Count; i++) + { + if (identitiesList[i].IsAuthenticated) + { + return identitiesList[i]; + } + } + } + else + { + foreach (var identity in claimsPrincipal.Identities) + { + if (identity.IsAuthenticated) + { + return identity; + } + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenSerializer.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenSerializer.cs new file mode 100644 index 0000000000..d71f2a2185 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenSerializer.cs @@ -0,0 +1,188 @@ +// 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 System; +using System.IO; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class DefaultAntiforgeryTokenSerializer : IAntiforgeryTokenSerializer + { + private static readonly string Purpose = "Microsoft.AspNetCore.Antiforgery.AntiforgeryToken.v1"; + private const byte TokenVersion = 0x01; + + private readonly IDataProtector _cryptoSystem; + private readonly ObjectPool _pool; + + public DefaultAntiforgeryTokenSerializer( + IDataProtectionProvider provider, + ObjectPool pool) + { + if (provider == null) + { + throw new ArgumentNullException(nameof(provider)); + } + + if (pool == null) + { + throw new ArgumentNullException(nameof(pool)); + } + + _cryptoSystem = provider.CreateProtector(Purpose); + _pool = pool; + } + + public AntiforgeryToken Deserialize(string serializedToken) + { + var serializationContext = _pool.Get(); + + Exception innerException = null; + try + { + var count = serializedToken.Length; + var charsRequired = WebEncoders.GetArraySizeRequiredToDecode(count); + var chars = serializationContext.GetChars(charsRequired); + var tokenBytes = WebEncoders.Base64UrlDecode( + serializedToken, + offset: 0, + buffer: chars, + bufferOffset: 0, + count: count); + + var unprotectedBytes = _cryptoSystem.Unprotect(tokenBytes); + var stream = serializationContext.Stream; + stream.Write(unprotectedBytes, offset: 0, count: unprotectedBytes.Length); + stream.Position = 0L; + + var reader = serializationContext.Reader; + var token = Deserialize(reader); + if (token != null) + { + return token; + } + } + catch (Exception ex) + { + // swallow all exceptions - homogenize error if something went wrong + innerException = ex; + } + finally + { + _pool.Return(serializationContext); + } + + // if we reached this point, something went wrong deserializing + throw new AntiforgeryValidationException(Resources.AntiforgeryToken_DeserializationFailed, innerException); + } + + /* The serialized format of the anti-XSRF token is as follows: + * Version: 1 byte integer + * SecurityToken: 16 byte binary blob + * IsCookieToken: 1 byte Boolean + * [if IsCookieToken != true] + * +- IsClaimsBased: 1 byte Boolean + * | [if IsClaimsBased = true] + * | `- ClaimUid: 32 byte binary blob + * | [if IsClaimsBased = false] + * | `- Username: UTF-8 string with 7-bit integer length prefix + * `- AdditionalData: UTF-8 string with 7-bit integer length prefix + */ + private static AntiforgeryToken Deserialize(BinaryReader reader) + { + // we can only consume tokens of the same serialized version that we generate + var embeddedVersion = reader.ReadByte(); + if (embeddedVersion != TokenVersion) + { + return null; + } + + var deserializedToken = new AntiforgeryToken(); + var securityTokenBytes = reader.ReadBytes(AntiforgeryToken.SecurityTokenBitLength / 8); + deserializedToken.SecurityToken = + new BinaryBlob(AntiforgeryToken.SecurityTokenBitLength, securityTokenBytes); + deserializedToken.IsCookieToken = reader.ReadBoolean(); + + if (!deserializedToken.IsCookieToken) + { + var isClaimsBased = reader.ReadBoolean(); + if (isClaimsBased) + { + var claimUidBytes = reader.ReadBytes(AntiforgeryToken.ClaimUidBitLength / 8); + deserializedToken.ClaimUid = new BinaryBlob(AntiforgeryToken.ClaimUidBitLength, claimUidBytes); + } + else + { + deserializedToken.Username = reader.ReadString(); + } + + deserializedToken.AdditionalData = reader.ReadString(); + } + + // if there's still unconsumed data in the stream, fail + if (reader.BaseStream.ReadByte() != -1) + { + return null; + } + + // success + return deserializedToken; + } + + public string Serialize(AntiforgeryToken token) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + var serializationContext = _pool.Get(); + + try + { + var writer = serializationContext.Writer; + writer.Write(TokenVersion); + writer.Write(token.SecurityToken.GetData()); + writer.Write(token.IsCookieToken); + + if (!token.IsCookieToken) + { + if (token.ClaimUid != null) + { + writer.Write(true /* isClaimsBased */); + writer.Write(token.ClaimUid.GetData()); + } + else + { + writer.Write(false /* isClaimsBased */); + writer.Write(token.Username); + } + + writer.Write(token.AdditionalData); + } + + writer.Flush(); + var stream = serializationContext.Stream; + var bytes = _cryptoSystem.Protect(stream.ToArray()); + + var count = bytes.Length; + var charsRequired = WebEncoders.GetArraySizeRequiredToEncode(count); + var chars = serializationContext.GetChars(charsRequired); + var outputLength = WebEncoders.Base64UrlEncode( + bytes, + offset: 0, + output: chars, + outputOffset: 0, + count: count); + + return new string(chars, startIndex: 0, length: outputLength); + } + finally + { + _pool.Return(serializationContext); + } + } + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenStore.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenStore.cs new file mode 100644 index 0000000000..95e6d6f1bc --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenStore.cs @@ -0,0 +1,90 @@ +// 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 System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class DefaultAntiforgeryTokenStore : IAntiforgeryTokenStore + { + private readonly AntiforgeryOptions _options; + + public DefaultAntiforgeryTokenStore(IOptions optionsAccessor) + { + if (optionsAccessor == null) + { + throw new ArgumentNullException(nameof(optionsAccessor)); + } + + _options = optionsAccessor.Value; + } + + public string GetCookieToken(HttpContext httpContext) + { + Debug.Assert(httpContext != null); + + var requestCookie = httpContext.Request.Cookies[_options.Cookie.Name]; + if (string.IsNullOrEmpty(requestCookie)) + { + // unable to find the cookie. + return null; + } + + return requestCookie; + } + + public async Task GetRequestTokensAsync(HttpContext httpContext) + { + Debug.Assert(httpContext != null); + + var cookieToken = httpContext.Request.Cookies[_options.Cookie.Name]; + + // We want to delay reading the form as much as possible, for example in case of large file uploads, + // request token could be part of the header. + StringValues requestToken; + if (_options.HeaderName != null) + { + requestToken = httpContext.Request.Headers[_options.HeaderName]; + } + + // Fall back to reading form instead + if (requestToken.Count == 0 && httpContext.Request.HasFormContentType) + { + // Check the content-type before accessing the form collection to make sure + // we report errors gracefully. + var form = await httpContext.Request.ReadFormAsync(); + requestToken = form[_options.FormFieldName]; + } + + return new AntiforgeryTokenSet(requestToken, cookieToken, _options.FormFieldName, _options.HeaderName); + } + + public void SaveCookieToken(HttpContext httpContext, string token) + { + Debug.Assert(httpContext != null); + Debug.Assert(token != null); + + var options = _options.Cookie.Build(httpContext); + + if (_options.Cookie.Path != null) + { + options.Path = _options.Cookie.Path.ToString(); + } + else + { + var pathBase = httpContext.Request.PathBase.ToString(); + if (!string.IsNullOrEmpty(pathBase)) + { + options.Path = pathBase; + } + } + + httpContext.Response.Cookies.Append(_options.Cookie.Name, token, options); + } + } +} diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultClaimUidExtractor.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultClaimUidExtractor.cs new file mode 100644 index 0000000000..1a7ef394a0 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultClaimUidExtractor.cs @@ -0,0 +1,149 @@ +// 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 System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Security.Claims; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + /// + /// Default implementation of . + /// + public class DefaultClaimUidExtractor : IClaimUidExtractor + { + private readonly ObjectPool _pool; + + public DefaultClaimUidExtractor(ObjectPool pool) + { + _pool = pool; + } + + /// + public string ExtractClaimUid(ClaimsPrincipal claimsPrincipal) + { + Debug.Assert(claimsPrincipal != null); + + var uniqueIdentifierParameters = GetUniqueIdentifierParameters(claimsPrincipal.Identities); + if (uniqueIdentifierParameters == null) + { + // No authenticated identities containing claims found. + return null; + } + + var claimUidBytes = ComputeSha256(uniqueIdentifierParameters); + return Convert.ToBase64String(claimUidBytes); + } + + public static IList GetUniqueIdentifierParameters(IEnumerable claimsIdentities) + { + var identitiesList = claimsIdentities as List; + if (identitiesList == null) + { + identitiesList = new List(claimsIdentities); + } + + for (var i = 0; i < identitiesList.Count; i++) + { + var identity = identitiesList[i]; + if (!identity.IsAuthenticated) + { + continue; + } + + var subClaim = identity.FindFirst( + claim => string.Equals("sub", claim.Type, StringComparison.Ordinal)); + if (subClaim != null && !string.IsNullOrEmpty(subClaim.Value)) + { + return new string[] + { + subClaim.Type, + subClaim.Value, + subClaim.Issuer + }; + } + + var nameIdentifierClaim = identity.FindFirst( + claim => string.Equals(ClaimTypes.NameIdentifier, claim.Type, StringComparison.Ordinal)); + if (nameIdentifierClaim != null && !string.IsNullOrEmpty(nameIdentifierClaim.Value)) + { + return new string[] + { + nameIdentifierClaim.Type, + nameIdentifierClaim.Value, + nameIdentifierClaim.Issuer + }; + } + + var upnClaim = identity.FindFirst( + claim => string.Equals(ClaimTypes.Upn, claim.Type, StringComparison.Ordinal)); + if (upnClaim != null && !string.IsNullOrEmpty(upnClaim.Value)) + { + return new string[] + { + upnClaim.Type, + upnClaim.Value, + upnClaim.Issuer + }; + } + } + + // We do not understand any of the ClaimsIdentity instances, fallback on serializing all claims in every claims Identity. + var allClaims = new List(); + for (var i = 0; i < identitiesList.Count; i++) + { + if (identitiesList[i].IsAuthenticated) + { + allClaims.AddRange(identitiesList[i].Claims); + } + } + + if (allClaims.Count == 0) + { + // No authenticated identities containing claims found. + return null; + } + + allClaims.Sort((a, b) => string.Compare(a.Type, b.Type, StringComparison.Ordinal)); + + var identifierParameters = new List(allClaims.Count * 3); + for (var i = 0; i < allClaims.Count; i++) + { + var claim = allClaims[i]; + identifierParameters.Add(claim.Type); + identifierParameters.Add(claim.Value); + identifierParameters.Add(claim.Issuer); + } + + return identifierParameters; + } + + private byte[] ComputeSha256(IEnumerable parameters) + { + var serializationContext = _pool.Get(); + + try + { + var writer = serializationContext.Writer; + foreach (string parameter in parameters) + { + writer.Write(parameter); // also writes the length as a prefix; unambiguous + } + + writer.Flush(); + + var sha256 = serializationContext.Sha256; + var stream = serializationContext.Stream; + var bytes = sha256.ComputeHash(stream.ToArray(), 0, checked((int)stream.Length)); + + return bytes; + } + finally + { + _pool.Return(serializationContext); + } + } + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryFeature.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryFeature.cs new file mode 100644 index 0000000000..2404359de7 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryFeature.cs @@ -0,0 +1,25 @@ +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public interface IAntiforgeryFeature + { + AntiforgeryToken CookieToken { get; set; } + + bool HaveDeserializedCookieToken { get; set; } + + bool HaveDeserializedRequestToken { get; set; } + + bool HaveGeneratedNewCookieToken { get; set; } + + bool HaveStoredNewCookieToken { get; set; } + + AntiforgeryToken NewCookieToken { get; set; } + + string NewCookieTokenString { get; set; } + + AntiforgeryToken NewRequestToken { get; set; } + + string NewRequestTokenString { get; set; } + + AntiforgeryToken RequestToken { get; set; } + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenGenerator.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenGenerator.cs new file mode 100644 index 0000000000..c0dff86047 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenGenerator.cs @@ -0,0 +1,50 @@ +// 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.Http; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + /// + /// Generates and validates antiforgery tokens. + /// + public interface IAntiforgeryTokenGenerator + { + /// + /// Generates a new random cookie token. + /// + /// An . + AntiforgeryToken GenerateCookieToken(); + + /// + /// Generates a request token corresponding to . + /// + /// The associated with the current request. + /// A valid cookie token. + /// An . + AntiforgeryToken GenerateRequestToken(HttpContext httpContext, AntiforgeryToken cookieToken); + + /// + /// Attempts to validate a cookie token. + /// + /// A valid cookie token. + /// true if the cookie token is valid, otherwise false. + bool IsCookieTokenValid(AntiforgeryToken cookieToken); + + /// + /// Attempts to validate a cookie and request token set for the given . + /// + /// The associated with the current request. + /// A cookie token. + /// A request token. + /// + /// Will be set to the validation message if the tokens are invalid, otherwise null. + /// + /// true if the tokens are valid, otherwise false. + bool TryValidateTokenSet( + HttpContext httpContext, + AntiforgeryToken cookieToken, + AntiforgeryToken requestToken, + out string message); + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenSerializer.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenSerializer.cs new file mode 100644 index 0000000000..134516e8c9 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenSerializer.cs @@ -0,0 +1,12 @@ +// 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. + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + // Abstracts out the serialization process for an antiforgery token + public interface IAntiforgeryTokenSerializer + { + AntiforgeryToken Deserialize(string serializedToken); + string Serialize(AntiforgeryToken token); + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenStore.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenStore.cs new file mode 100644 index 0000000000..1b4aa8ec05 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenStore.cs @@ -0,0 +1,22 @@ +// 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 System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public interface IAntiforgeryTokenStore + { + string GetCookieToken(HttpContext httpContext); + + /// + /// Gets the cookie and request tokens from the request. + /// + /// The for the current request. + /// The . + Task GetRequestTokensAsync(HttpContext httpContext); + + void SaveCookieToken(HttpContext httpContext, string token); + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IClaimUidExtractor.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IClaimUidExtractor.cs new file mode 100644 index 0000000000..72ab230fb4 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IClaimUidExtractor.cs @@ -0,0 +1,21 @@ +// 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 System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + /// + /// This interface can extract unique identifers for a . + /// + public interface IClaimUidExtractor + { + /// + /// Extracts claims identifier. + /// + /// The . + /// The claims identifier. + string ExtractClaimUid(ClaimsPrincipal claimsPrincipal); + } +} \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Microsoft.AspNetCore.Antiforgery.csproj b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Microsoft.AspNetCore.Antiforgery.csproj new file mode 100644 index 0000000000..e196bf7f56 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Microsoft.AspNetCore.Antiforgery.csproj @@ -0,0 +1,19 @@ + + + + An antiforgery system for ASP.NET Core designed to generate and validate tokens to prevent Cross-Site Request Forgery attacks. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;antiforgery + + + + + + + + + + + diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Properties/AssemblyInfo.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..490fb19533 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// 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 System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Antiforgery.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Properties/Resources.Designer.cs b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..83811ea2dc --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Properties/Resources.Designer.cs @@ -0,0 +1,254 @@ +// +namespace Microsoft.AspNetCore.Antiforgery +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Antiforgery.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The provided identity of type '{0}' is marked {1} = {2} but does not have a value for {3}. By default, the antiforgery system requires that all authenticated identities have a unique {3}. If it is not possible to provide a unique {3} for this identity, consider extending {4} by overriding the {5} or a custom type that can provide some form of unique identifier for the current user. + /// + internal static string AntiforgeryTokenValidator_AuthenticatedUserWithoutUsername + { + get => GetString("AntiforgeryTokenValidator_AuthenticatedUserWithoutUsername"); + } + + /// + /// The provided identity of type '{0}' is marked {1} = {2} but does not have a value for {3}. By default, the antiforgery system requires that all authenticated identities have a unique {3}. If it is not possible to provide a unique {3} for this identity, consider extending {4} by overriding the {5} or a custom type that can provide some form of unique identifier for the current user. + /// + internal static string FormatAntiforgeryTokenValidator_AuthenticatedUserWithoutUsername(object p0, object p1, object p2, object p3, object p4, object p5) + => string.Format(CultureInfo.CurrentCulture, GetString("AntiforgeryTokenValidator_AuthenticatedUserWithoutUsername"), p0, p1, p2, p3, p4, p5); + + /// + /// The provided antiforgery token failed a custom data check. + /// + internal static string AntiforgeryToken_AdditionalDataCheckFailed + { + get => GetString("AntiforgeryToken_AdditionalDataCheckFailed"); + } + + /// + /// The provided antiforgery token failed a custom data check. + /// + internal static string FormatAntiforgeryToken_AdditionalDataCheckFailed() + => GetString("AntiforgeryToken_AdditionalDataCheckFailed"); + + /// + /// The provided antiforgery token was meant for a different claims-based user than the current user. + /// + internal static string AntiforgeryToken_ClaimUidMismatch + { + get => GetString("AntiforgeryToken_ClaimUidMismatch"); + } + + /// + /// The provided antiforgery token was meant for a different claims-based user than the current user. + /// + internal static string FormatAntiforgeryToken_ClaimUidMismatch() + => GetString("AntiforgeryToken_ClaimUidMismatch"); + + /// + /// The antiforgery token could not be decrypted. + /// + internal static string AntiforgeryToken_DeserializationFailed + { + get => GetString("AntiforgeryToken_DeserializationFailed"); + } + + /// + /// The antiforgery token could not be decrypted. + /// + internal static string FormatAntiforgeryToken_DeserializationFailed() + => GetString("AntiforgeryToken_DeserializationFailed"); + + /// + /// The antiforgery cookie token and request token do not match. + /// + internal static string AntiforgeryToken_SecurityTokenMismatch + { + get => GetString("AntiforgeryToken_SecurityTokenMismatch"); + } + + /// + /// The antiforgery cookie token and request token do not match. + /// + internal static string FormatAntiforgeryToken_SecurityTokenMismatch() + => GetString("AntiforgeryToken_SecurityTokenMismatch"); + + /// + /// Validation of the provided antiforgery token failed. The cookie token and the request token were swapped. + /// + internal static string AntiforgeryToken_TokensSwapped + { + get => GetString("AntiforgeryToken_TokensSwapped"); + } + + /// + /// Validation of the provided antiforgery token failed. The cookie token and the request token were swapped. + /// + internal static string FormatAntiforgeryToken_TokensSwapped() + => GetString("AntiforgeryToken_TokensSwapped"); + + /// + /// The provided antiforgery token was meant for user "{0}", but the current user is "{1}". + /// + internal static string AntiforgeryToken_UsernameMismatch + { + get => GetString("AntiforgeryToken_UsernameMismatch"); + } + + /// + /// The provided antiforgery token was meant for user "{0}", but the current user is "{1}". + /// + internal static string FormatAntiforgeryToken_UsernameMismatch(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("AntiforgeryToken_UsernameMismatch"), p0, p1); + + /// + /// The antiforgery cookie token is invalid. + /// + internal static string Antiforgery_CookieToken_IsInvalid + { + get => GetString("Antiforgery_CookieToken_IsInvalid"); + } + + /// + /// The antiforgery cookie token is invalid. + /// + internal static string FormatAntiforgery_CookieToken_IsInvalid() + => GetString("Antiforgery_CookieToken_IsInvalid"); + + /// + /// The required antiforgery cookie "{0}" is not present. + /// + internal static string Antiforgery_CookieToken_MustBeProvided + { + get => GetString("Antiforgery_CookieToken_MustBeProvided"); + } + + /// + /// The required antiforgery cookie "{0}" is not present. + /// + internal static string FormatAntiforgery_CookieToken_MustBeProvided(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Antiforgery_CookieToken_MustBeProvided"), p0); + + /// + /// The required antiforgery cookie token must be provided. + /// + internal static string Antiforgery_CookieToken_MustBeProvided_Generic + { + get => GetString("Antiforgery_CookieToken_MustBeProvided_Generic"); + } + + /// + /// The required antiforgery cookie token must be provided. + /// + internal static string FormatAntiforgery_CookieToken_MustBeProvided_Generic() + => GetString("Antiforgery_CookieToken_MustBeProvided_Generic"); + + /// + /// The required antiforgery form field "{0}" is not present. + /// + internal static string Antiforgery_FormToken_MustBeProvided + { + get => GetString("Antiforgery_FormToken_MustBeProvided"); + } + + /// + /// The required antiforgery form field "{0}" is not present. + /// + internal static string FormatAntiforgery_FormToken_MustBeProvided(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Antiforgery_FormToken_MustBeProvided"), p0); + + /// + /// The required antiforgery header value "{0}" is not present. + /// + internal static string Antiforgery_HeaderToken_MustBeProvided + { + get => GetString("Antiforgery_HeaderToken_MustBeProvided"); + } + + /// + /// The required antiforgery header value "{0}" is not present. + /// + internal static string FormatAntiforgery_HeaderToken_MustBeProvided(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Antiforgery_HeaderToken_MustBeProvided"), p0); + + /// + /// The required antiforgery request token was not provided in either form field "{0}" or header value "{1}". + /// + internal static string Antiforgery_RequestToken_MustBeProvided + { + get => GetString("Antiforgery_RequestToken_MustBeProvided"); + } + + /// + /// The required antiforgery request token was not provided in either form field "{0}" or header value "{1}". + /// + internal static string FormatAntiforgery_RequestToken_MustBeProvided(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("Antiforgery_RequestToken_MustBeProvided"), p0, p1); + + /// + /// The required antiforgery request token must be provided. + /// + internal static string Antiforgery_RequestToken_MustBeProvided_Generic + { + get => GetString("Antiforgery_RequestToken_MustBeProvided_Generic"); + } + + /// + /// The required antiforgery request token must be provided. + /// + internal static string FormatAntiforgery_RequestToken_MustBeProvided_Generic() + => GetString("Antiforgery_RequestToken_MustBeProvided_Generic"); + + /// + /// The antiforgery system has the configuration value {optionName} = {value}, but the current request is not an SSL request. + /// + internal static string Antiforgery_RequiresSSL + { + get => GetString("Antiforgery_RequiresSSL"); + } + + /// + /// The antiforgery system has the configuration value {optionName} = {value}, but the current request is not an SSL request. + /// + internal static string FormatAntiforgery_RequiresSSL(object optionName, object value) + => string.Format(CultureInfo.CurrentCulture, GetString("Antiforgery_RequiresSSL", "optionName", "value"), optionName, value); + + /// + /// Value cannot be null or empty. + /// + internal static string ArgumentCannotBeNullOrEmpty + { + get => GetString("ArgumentCannotBeNullOrEmpty"); + } + + /// + /// Value cannot be null or empty. + /// + internal static string FormatArgumentCannotBeNullOrEmpty() + => GetString("ArgumentCannotBeNullOrEmpty"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Resources.resx b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Resources.resx new file mode 100644 index 0000000000..eeda70bc63 --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Resources.resx @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The provided identity of type '{0}' is marked {1} = {2} but does not have a value for {3}. By default, the antiforgery system requires that all authenticated identities have a unique {3}. If it is not possible to provide a unique {3} for this identity, consider extending {4} by overriding the {5} or a custom type that can provide some form of unique identifier for the current user. + 0 = typeof(identity), 1 = nameof(IsAuthenticated), 2 = bool.TrueString, 3 = nameof(Name), 4 = nameof(IAdditionalDataProvider), 5 = nameof(DefaultAdditionalDataProvider) + + + The provided antiforgery token failed a custom data check. + + + The provided antiforgery token was meant for a different claims-based user than the current user. + + + The antiforgery token could not be decrypted. + + + The antiforgery cookie token and request token do not match. + + + Validation of the provided antiforgery token failed. The cookie token and the request token were swapped. + + + The provided antiforgery token was meant for user "{0}", but the current user is "{1}". + + + The antiforgery cookie token is invalid. + + + The required antiforgery cookie "{0}" is not present. + + + The required antiforgery cookie token must be provided. + + + The required antiforgery form field "{0}" is not present. + + + The required antiforgery header value "{0}" is not present. + + + The required antiforgery request token was not provided in either form field "{0}" or header value "{1}". + + + The required antiforgery request token must be provided. + + + The antiforgery system has the configuration value {optionName} = {value}, but the current request is not an SSL request. + + + Value cannot be null or empty. + + \ No newline at end of file diff --git a/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/baseline.netcore.json b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/baseline.netcore.json new file mode 100644 index 0000000000..eaa03254ea --- /dev/null +++ b/src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/baseline.netcore.json @@ -0,0 +1,456 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Antiforgery, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.AntiforgeryServiceCollectionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddAntiforgery", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddAntiforgery", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "setupAction", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Antiforgery.AntiforgeryOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Cookie", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Cookie", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieBuilder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FormFieldName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_FormFieldName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HeaderName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HeaderName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SuppressXFrameOptionsHeader", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SuppressXFrameOptionsHeader", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookiePath", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookiePath", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieDomain", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieDomain", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequireSsl", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequireSsl", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "DefaultCookiePrefix", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Antiforgery.AntiforgeryTokenSet", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequestToken", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FormFieldName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HeaderName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieToken", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "requestToken", + "Type": "System.String" + }, + { + "Name": "cookieToken", + "Type": "System.String" + }, + { + "Name": "formFieldName", + "Type": "System.String" + }, + { + "Name": "headerName", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.Exception", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "message", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "message", + "Type": "System.String" + }, + { + "Name": "innerException", + "Type": "System.Exception" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Antiforgery.IAntiforgery", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetAndStoreTokens", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Antiforgery.AntiforgeryTokenSet", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetTokens", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Antiforgery.AntiforgeryTokenSet", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsRequestValidAsync", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ValidateRequestAsync", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetCookieTokenAndHeader", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Antiforgery.IAntiforgeryAdditionalDataProvider", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetAdditionalData", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ValidateAdditionalData", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "additionalData", + "Type": "System.String" + } + ], + "ReturnType": "System.Boolean", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Antiforgery/test/Directory.Build.props b/src/Antiforgery/test/Directory.Build.props new file mode 100644 index 0000000000..eb4ed371f3 --- /dev/null +++ b/src/Antiforgery/test/Directory.Build.props @@ -0,0 +1,10 @@ + + + + + netcoreapp2.1 + $(DeveloperBuildTestTfms) + netcoreapp2.1;netcoreapp2.0 + $(StandardTestTfms);net461 + + \ No newline at end of file diff --git a/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/AntiforgeryOptionsSetupTest.cs b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/AntiforgeryOptionsSetupTest.cs new file mode 100644 index 0000000000..b0acff572d --- /dev/null +++ b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/AntiforgeryOptionsSetupTest.cs @@ -0,0 +1,73 @@ +// 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.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class AntiforgeryOptionsSetupTest + { + [Theory] + [InlineData("HelloWorldApp", ".AspNetCore.Antiforgery.tGmK82_ckDw")] + [InlineData("TodoCalendar", ".AspNetCore.Antiforgery.7mK1hBEBwYs")] + public void AntiforgeryOptionsSetup_SetsDefaultCookieName_BasedOnApplicationId( + string applicationId, + string expectedCookieName) + { + // Arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.AddAntiforgery(); + serviceCollection + .AddDataProtection() + .SetApplicationName(applicationId); + + var services = serviceCollection.BuildServiceProvider(); + var options = services.GetRequiredService>(); + + // Act + var cookieName = options.Value.Cookie.Name; + + // Assert + Assert.Equal(expectedCookieName, cookieName); + } + + [Fact] + public void AntiforgeryOptionsSetup_UserOptionsSetup_CanSetCookieName() + { + // Arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.Configure(o => + { + Assert.Null(o.Cookie.Name); + o.Cookie.Name = "antiforgery"; + }); + serviceCollection.AddAntiforgery(); + serviceCollection + .AddDataProtection() + .SetApplicationName("HelloWorldApp"); + + var services = serviceCollection.BuildServiceProvider(); + var options = services.GetRequiredService>(); + + // Act + var cookieName = options.Value.Cookie.Name; + + // Assert + Assert.Equal("antiforgery", cookieName); + } + + [Fact] + public void AntiforgeryOptions_SetsCookieSecurePolicy_ToNone_ByDefault() + { + // Arrange & Act + var options = new AntiforgeryOptions(); + + // Assert + Assert.Equal(CookieSecurePolicy.None, options.Cookie.SecurePolicy); + } + } +} diff --git a/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/AntiforgeryTokenTest.cs b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/AntiforgeryTokenTest.cs new file mode 100644 index 0000000000..9cafd306b0 --- /dev/null +++ b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/AntiforgeryTokenTest.cs @@ -0,0 +1,132 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class AntiforgeryTokenTest + { + [Fact] + public void AdditionalDataProperty() + { + // Arrange + var token = new AntiforgeryToken(); + + // Act & assert - 1 + Assert.Equal("", token.AdditionalData); + + // Act & assert - 2 + token.AdditionalData = "additional data"; + Assert.Equal("additional data", token.AdditionalData); + + // Act & assert - 3 + token.AdditionalData = null; + Assert.Equal("", token.AdditionalData); + } + + [Fact] + public void ClaimUidProperty() + { + // Arrange + var token = new AntiforgeryToken(); + + // Act & assert - 1 + Assert.Null(token.ClaimUid); + + // Act & assert - 2 + BinaryBlob blob = new BinaryBlob(32); + token.ClaimUid = blob; + Assert.Equal(blob, token.ClaimUid); + + // Act & assert - 3 + token.ClaimUid = null; + Assert.Null(token.ClaimUid); + } + + [Fact] + public void IsCookieTokenProperty() + { + // Arrange + var token = new AntiforgeryToken(); + + // Act & assert - 1 + Assert.False(token.IsCookieToken); + + // Act & assert - 2 + token.IsCookieToken = true; + Assert.True(token.IsCookieToken); + + // Act & assert - 3 + token.IsCookieToken = false; + Assert.False(token.IsCookieToken); + } + + [Fact] + public void UsernameProperty() + { + // Arrange + var token = new AntiforgeryToken(); + + // Act & assert - 1 + Assert.Equal("", token.Username); + + // Act & assert - 2 + token.Username = "my username"; + Assert.Equal("my username", token.Username); + + // Act & assert - 3 + token.Username = null; + Assert.Equal("", token.Username); + } + + [Fact] + public void SecurityTokenProperty_GetsAutopopulated() + { + // Arrange + var token = new AntiforgeryToken(); + + // Act + var securityToken = token.SecurityToken; + + // Assert + Assert.NotNull(securityToken); + Assert.Equal(AntiforgeryToken.SecurityTokenBitLength, securityToken.BitLength); + + // check that we're not making a new one each property call + Assert.Equal(securityToken, token.SecurityToken); + } + + [Fact] + public void SecurityTokenProperty_PropertySetter_DoesNotUseDefaults() + { + // Arrange + var token = new AntiforgeryToken(); + + // Act + var securityToken = new BinaryBlob(64); + token.SecurityToken = securityToken; + + // Assert + Assert.Equal(securityToken, token.SecurityToken); + } + + [Fact] + public void SecurityTokenProperty_PropertySetter_DoesNotAllowNulls() + { + // Arrange + var token = new AntiforgeryToken(); + + // Act + token.SecurityToken = null; + var securityToken = token.SecurityToken; + + // Assert + Assert.NotNull(securityToken); + Assert.Equal(AntiforgeryToken.SecurityTokenBitLength, securityToken.BitLength); + + // check that we're not making a new one each property call + Assert.Equal(securityToken, token.SecurityToken); + } + } +} \ No newline at end of file diff --git a/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/BinaryBlobTest.cs b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/BinaryBlobTest.cs new file mode 100644 index 0000000000..2ab5b12fc1 --- /dev/null +++ b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/BinaryBlobTest.cs @@ -0,0 +1,129 @@ +// 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 System; +using Xunit; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class BinaryBlobTest + { + [Fact] + public void Ctor_BitLength() + { + // Act + var blob = new BinaryBlob(bitLength: 64); + var data = blob.GetData(); + + // Assert + Assert.Equal(64, blob.BitLength); + Assert.Equal(64 / 8, data.Length); + Assert.NotEqual(new byte[64 / 8], data); // should not be a zero-filled array + } + + [Theory] + [InlineData(24)] + [InlineData(33)] + public void Ctor_BitLength_Bad(int bitLength) + { + // Act & assert + var ex = Assert.Throws(() => new BinaryBlob(bitLength)); + Assert.Equal("bitLength", ex.ParamName); + } + + [Fact] + public void Ctor_BitLength_ProducesDifferentValues() + { + // Act + var blobA = new BinaryBlob(bitLength: 64); + var blobB = new BinaryBlob(bitLength: 64); + + // Assert + Assert.NotEqual(blobA.GetData(), blobB.GetData()); + } + + [Fact] + public void Ctor_Data() + { + // Arrange + var expectedData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + + // Act + var blob = new BinaryBlob(32, expectedData); + + // Assert + Assert.Equal(32, blob.BitLength); + Assert.Equal(expectedData, blob.GetData()); + } + + [Theory] + [InlineData((object[])null)] + [InlineData(new byte[] { 0x01, 0x02, 0x03 })] + public void Ctor_Data_Bad(byte[] data) + { + // Act & assert + var ex = Assert.Throws(() => new BinaryBlob(32, data)); + Assert.Equal("data", ex.ParamName); + } + + [Fact] + public void Equals_DifferentData_ReturnsFalse() + { + // Arrange + object blobA = new BinaryBlob(32, new byte[] { 0x01, 0x02, 0x03, 0x04 }); + object blobB = new BinaryBlob(32, new byte[] { 0x04, 0x03, 0x02, 0x01 }); + + // Act & assert + Assert.NotEqual(blobA, blobB); + } + + [Fact] + public void Equals_NotABlob_ReturnsFalse() + { + // Arrange + object blobA = new BinaryBlob(32); + object blobB = "hello"; + + // Act & assert + Assert.NotEqual(blobA, blobB); + } + + [Fact] + public void Equals_Null_ReturnsFalse() + { + // Arrange + object blobA = new BinaryBlob(32); + object blobB = null; + + // Act & assert + Assert.NotEqual(blobA, blobB); + } + + [Fact] + public void Equals_SameData_ReturnsTrue() + { + // Arrange + object blobA = new BinaryBlob(32, new byte[] { 0x01, 0x02, 0x03, 0x04 }); + object blobB = new BinaryBlob(32, new byte[] { 0x01, 0x02, 0x03, 0x04 }); + + // Act & assert + Assert.Equal(blobA, blobB); + } + + [Fact] + public void GetHashCodeTest() + { + // Arrange + var blobData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + var expectedHashCode = BitConverter.ToInt32(blobData, 0); + + var blob = new BinaryBlob(32, blobData); + + // Act + var actualHashCode = blob.GetHashCode(); + + // Assert + Assert.Equal(expectedHashCode, actualHashCode); + } + } +} \ No newline at end of file diff --git a/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTest.cs b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTest.cs new file mode 100644 index 0000000000..faf895d524 --- /dev/null +++ b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTest.cs @@ -0,0 +1,1497 @@ +// 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 System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class DefaultAntiforgeryTest + { + private const string ResponseCacheHeadersOverrideWarningMessage = + "The 'Cache-Control' and 'Pragma' headers have been overridden and set to 'no-cache, no-store' and " + + "'no-cache' respectively to prevent caching of this response. Any response that uses antiforgery " + + "should not be cached."; + + [Fact] + public async Task ChecksSSL_ValidateRequestAsync_Throws() + { + // Arrange + var httpContext = GetHttpContext(); + var options = new AntiforgeryOptions + { +#pragma warning disable CS0618 + // obsolete property still forwards to correctly to the new API + RequireSsl = true +#pragma warning restore CS0618 + }; + var antiforgery = GetAntiforgery(httpContext, options); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => antiforgery.ValidateRequestAsync(httpContext)); + Assert.Equal( + @"The antiforgery system has the configuration value AntiforgeryOptions.Cookie.SecurePolicy = Always, " + + "but the current request is not an SSL request.", + exception.Message); + } + + [Fact] + public async Task ChecksSSL_IsRequestValidAsync_Throws() + { + // Arrange + var httpContext = GetHttpContext(); + var options = new AntiforgeryOptions() + { + Cookie = { SecurePolicy = CookieSecurePolicy.Always } + }; + + var antiforgery = GetAntiforgery(httpContext, options); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => antiforgery.IsRequestValidAsync(httpContext)); + Assert.Equal( + @"The antiforgery system has the configuration value AntiforgeryOptions.Cookie.SecurePolicy = Always, " + + "but the current request is not an SSL request.", + exception.Message); + } + + [Fact] + public void ChecksSSL_GetAndStoreTokens_Throws() + { + // Arrange + var httpContext = GetHttpContext(); + var options = new AntiforgeryOptions() + { + Cookie = { SecurePolicy = CookieSecurePolicy.Always } + }; + + var antiforgery = GetAntiforgery(httpContext, options); + + // Act & Assert + var exception = Assert.Throws( + () => antiforgery.GetAndStoreTokens(httpContext)); + Assert.Equal( + @"The antiforgery system has the configuration value AntiforgeryOptions.Cookie.SecurePolicy = Always, " + + "but the current request is not an SSL request.", + exception.Message); + } + + [Fact] + public void ChecksSSL_GetTokens_Throws() + { + // Arrange + var httpContext = GetHttpContext(); + var options = new AntiforgeryOptions() + { + Cookie = { SecurePolicy = CookieSecurePolicy.Always } + }; + + var antiforgery = GetAntiforgery(httpContext, options); + + // Act & Assert + var exception = Assert.Throws( + () => antiforgery.GetTokens(httpContext)); + Assert.Equal( + @"The antiforgery system has the configuration value AntiforgeryOptions.Cookie.SecurePolicy = Always, " + + "but the current request is not an SSL request.", + exception.Message); + } + + [Fact] + public void ChecksSSL_SetCookieTokenAndHeader_Throws() + { + // Arrange + var httpContext = GetHttpContext(); + var options = new AntiforgeryOptions() + { + Cookie = { SecurePolicy = CookieSecurePolicy.Always } + }; + + var antiforgery = GetAntiforgery(httpContext, options); + + // Act & Assert + var exception = Assert.Throws( + () => antiforgery.SetCookieTokenAndHeader(httpContext)); + Assert.Equal( + @"The antiforgery system has the configuration value AntiforgeryOptions.Cookie.SecurePolicy = Always, " + + "but the current request is not an SSL request.", + exception.Message); + } + + [Fact] + public void GetTokens_ExistingInvalidCookieToken_GeneratesANewCookieTokenAndANewFormToken() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature(); + // Generate a new cookie. + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: false, + isOldCookieValid: false, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + + // Act + var tokenset = antiforgery.GetTokens(context.HttpContext); + + // Assert + Assert.Equal(context.TestTokenSet.NewCookieTokenString, tokenset.CookieToken); + Assert.Equal(context.TestTokenSet.FormTokenString, tokenset.RequestToken); + + Assert.NotNull(antiforgeryFeature); + Assert.True(antiforgeryFeature.HaveDeserializedCookieToken); + Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken); + Assert.True(antiforgeryFeature.HaveGeneratedNewCookieToken); + Assert.Equal(context.TestTokenSet.NewCookieToken, antiforgeryFeature.NewCookieToken); + Assert.Equal(context.TestTokenSet.NewCookieTokenString, antiforgeryFeature.NewCookieTokenString); + Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.NewRequestToken); + Assert.Equal(context.TestTokenSet.FormTokenString, antiforgeryFeature.NewRequestTokenString); + } + + [Fact] + public void GetTokens_ExistingInvalidCookieToken_SwallowsExceptions() + { + // Arrange + // Make sure the existing cookie is invalid. + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: false, + isOldCookieValid: false); + + // Exception will cause the cookieToken to be null. + context.TokenSerializer + .Setup(o => o.Deserialize(context.TestTokenSet.OldCookieTokenString)) + .Throws(new Exception("should be swallowed")); + context.TokenGenerator + .Setup(o => o.IsCookieTokenValid(null)) + .Returns(false); + + var antiforgery = GetAntiforgery(context); + + // Act + var tokenset = antiforgery.GetTokens(context.HttpContext); + + // Assert + Assert.Equal(context.TestTokenSet.NewCookieTokenString, tokenset.CookieToken); + Assert.Equal(context.TestTokenSet.FormTokenString, tokenset.RequestToken); + } + + [Fact] + public void GetTokens_ExistingValidCookieToken_GeneratesANewFormToken() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature(); + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: true, + isOldCookieValid: true, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + + // Act + var tokenset = antiforgery.GetTokens(context.HttpContext); + + // Assert + Assert.Null(tokenset.CookieToken); + Assert.Equal(context.TestTokenSet.FormTokenString, tokenset.RequestToken); + + Assert.NotNull(antiforgeryFeature); + Assert.True(antiforgeryFeature.HaveDeserializedCookieToken); + Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken); + Assert.True(antiforgeryFeature.HaveGeneratedNewCookieToken); + Assert.Null(antiforgeryFeature.NewCookieToken); + Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.NewRequestToken); + Assert.Equal(context.TestTokenSet.FormTokenString, antiforgeryFeature.NewRequestTokenString); + } + + [Fact] + public void GetTokens_DoesNotSerializeTwice() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature + { + HaveDeserializedCookieToken = true, + HaveGeneratedNewCookieToken = true, + NewRequestToken = new AntiforgeryToken(), + NewRequestTokenString = "serialized-form-token-from-context", + }; + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: true, + isOldCookieValid: true, + antiforgeryFeature: antiforgeryFeature); + + var antiforgery = GetAntiforgery(context); + + // Act + var tokenset = antiforgery.GetTokens(context.HttpContext); + + // Assert + Assert.Null(tokenset.CookieToken); + Assert.Equal("serialized-form-token-from-context", tokenset.RequestToken); + + Assert.Null(antiforgeryFeature.NewCookieToken); + + // Token serializer not used. + context.TokenSerializer.Verify( + o => o.Deserialize(It.IsAny()), + Times.Never); + context.TokenSerializer.Verify( + o => o.Serialize(It.IsAny()), + Times.Never); + } + + [Fact] + public void GetAndStoreTokens_ExistingValidCookieToken_NotOverriden() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature(); + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: true, + isOldCookieValid: true, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + + // Act + var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext); + + // Assert + // We shouldn't have saved the cookie because it already existed. + context.TokenStore.Verify( + t => t.SaveCookieToken(It.IsAny(), It.IsAny()), + Times.Never); + + Assert.Null(tokenSet.CookieToken); + Assert.Equal(context.TestTokenSet.FormTokenString, tokenSet.RequestToken); + + Assert.NotNull(antiforgeryFeature); + Assert.True(antiforgeryFeature.HaveDeserializedCookieToken); + Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken); + Assert.True(antiforgeryFeature.HaveGeneratedNewCookieToken); + Assert.Null(antiforgeryFeature.NewCookieToken); + Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.NewRequestToken); + Assert.Equal(context.TestTokenSet.FormTokenString, antiforgeryFeature.NewRequestTokenString); + } + + [Fact] + public void GetAndStoreTokens_ExistingValidCookieToken_NotOverriden_AndSetsDoNotCacheHeaders() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature(); + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: true, + isOldCookieValid: true, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + + // Act + var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext); + + // Assert + // We shouldn't have saved the cookie because it already existed. + context.TokenStore.Verify( + t => t.SaveCookieToken(It.IsAny(), It.IsAny()), + Times.Never); + + Assert.Null(tokenSet.CookieToken); + Assert.Equal(context.TestTokenSet.FormTokenString, tokenSet.RequestToken); + + Assert.NotNull(antiforgeryFeature); + Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken); + Assert.Equal("no-cache, no-store", context.HttpContext.Response.Headers[HeaderNames.CacheControl]); + Assert.Equal("no-cache", context.HttpContext.Response.Headers[HeaderNames.Pragma]); + } + + [Fact] + public void GetAndStoreTokens_ExistingCachingHeaders_Overriden() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature(); + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: true, + isOldCookieValid: true, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + context.HttpContext.Response.Headers["Cache-Control"] = "public"; + + // Act + var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext); + + // Assert + // We shouldn't have saved the cookie because it already existed. + context.TokenStore.Verify( + t => t.SaveCookieToken(It.IsAny(), It.IsAny()), + Times.Never); + + Assert.Null(tokenSet.CookieToken); + Assert.Equal(context.TestTokenSet.FormTokenString, tokenSet.RequestToken); + + Assert.NotNull(antiforgeryFeature); + Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken); + Assert.Equal("no-cache, no-store", context.HttpContext.Response.Headers[HeaderNames.CacheControl]); + Assert.Equal("no-cache", context.HttpContext.Response.Headers[HeaderNames.Pragma]); + } + + [Fact] + public void GetAndStoreTokens_NoExistingCookieToken_Saved() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature(); + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: false, + isOldCookieValid: false, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + + // Act + var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext); + + // Assert + context.TokenStore.Verify( + t => t.SaveCookieToken(It.IsAny(), context.TestTokenSet.NewCookieTokenString), + Times.Once); + + Assert.Equal(context.TestTokenSet.NewCookieTokenString, tokenSet.CookieToken); + Assert.Equal(context.TestTokenSet.FormTokenString, tokenSet.RequestToken); + + Assert.NotNull(antiforgeryFeature); + Assert.True(antiforgeryFeature.HaveDeserializedCookieToken); + Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken); + Assert.True(antiforgeryFeature.HaveGeneratedNewCookieToken); + Assert.Equal(context.TestTokenSet.NewCookieToken, antiforgeryFeature.NewCookieToken); + Assert.Equal(context.TestTokenSet.NewCookieTokenString, antiforgeryFeature.NewCookieTokenString); + Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.NewRequestToken); + Assert.Equal(context.TestTokenSet.FormTokenString, antiforgeryFeature.NewRequestTokenString); + Assert.True(antiforgeryFeature.HaveStoredNewCookieToken); + } + + [Fact] + public void GetAndStoreTokens_NoExistingCookieToken_Saved_AndSetsDoNotCacheHeaders() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature(); + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: false, + isOldCookieValid: false, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + + // Act + var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext); + + // Assert + context.TokenStore.Verify( + t => t.SaveCookieToken(It.IsAny(), context.TestTokenSet.NewCookieTokenString), + Times.Once); + + Assert.Equal(context.TestTokenSet.NewCookieTokenString, tokenSet.CookieToken); + Assert.Equal(context.TestTokenSet.FormTokenString, tokenSet.RequestToken); + + Assert.NotNull(antiforgeryFeature); + Assert.True(antiforgeryFeature.HaveDeserializedCookieToken); + Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken); + Assert.Equal("no-cache, no-store", context.HttpContext.Response.Headers[HeaderNames.CacheControl]); + Assert.Equal("no-cache", context.HttpContext.Response.Headers[HeaderNames.Pragma]); + } + + [Fact] + public void GetAndStoreTokens_DoesNotSerializeTwice() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature + { + HaveDeserializedCookieToken = true, + HaveGeneratedNewCookieToken = true, + NewCookieToken = new AntiforgeryToken(), + NewCookieTokenString = "serialized-cookie-token-from-context", + NewRequestToken = new AntiforgeryToken(), + NewRequestTokenString = "serialized-form-token-from-context", + }; + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: true, + isOldCookieValid: true, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + + context.TokenStore + .Setup(t => t.SaveCookieToken(context.HttpContext, "serialized-cookie-token-from-context")) + .Verifiable(); + + // Act + var tokenset = antiforgery.GetAndStoreTokens(context.HttpContext); + + // Assert + // Token store used once, with expected arguments. + // Passed context's cookie token though request's cookie token was valid. + context.TokenStore.Verify( + t => t.SaveCookieToken(context.HttpContext, "serialized-cookie-token-from-context"), + Times.Once); + + // Token serializer not used. + context.TokenSerializer.Verify( + o => o.Deserialize(It.IsAny()), + Times.Never); + context.TokenSerializer.Verify( + o => o.Serialize(It.IsAny()), + Times.Never); + + Assert.Equal("serialized-cookie-token-from-context", tokenset.CookieToken); + Assert.Equal("serialized-form-token-from-context", tokenset.RequestToken); + + Assert.True(antiforgeryFeature.HaveStoredNewCookieToken); + } + + [Fact] + public void GetAndStoreTokens_DoesNotStoreTwice() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature + { + HaveDeserializedCookieToken = true, + HaveGeneratedNewCookieToken = true, + HaveStoredNewCookieToken = true, + NewCookieToken = new AntiforgeryToken(), + NewCookieTokenString = "serialized-cookie-token-from-context", + NewRequestToken = new AntiforgeryToken(), + NewRequestTokenString = "serialized-form-token-from-context", + }; + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: true, + isOldCookieValid: true, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + + // Act + var tokenset = antiforgery.GetAndStoreTokens(context.HttpContext); + + // Assert + // Token store not used. + context.TokenStore.Verify( + t => t.SaveCookieToken(It.IsAny(), It.IsAny()), + Times.Never); + + // Token serializer not used. + context.TokenSerializer.Verify( + o => o.Deserialize(It.IsAny()), + Times.Never); + context.TokenSerializer.Verify( + o => o.Serialize(It.IsAny()), + Times.Never); + + Assert.Equal("serialized-cookie-token-from-context", tokenset.CookieToken); + Assert.Equal("serialized-form-token-from-context", tokenset.RequestToken); + } + + [Fact] + public async Task IsRequestValidAsync_FromStore_Failure() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature(); + var context = CreateMockContext(new AntiforgeryOptions(), antiforgeryFeature: antiforgeryFeature); + + string message; + context.TokenGenerator + .Setup(o => o.TryValidateTokenSet( + context.HttpContext, + context.TestTokenSet.OldCookieToken, + context.TestTokenSet.RequestToken, + out message)) + .Returns(false); + + var antiforgery = GetAntiforgery(context); + + // Act + var result = await antiforgery.IsRequestValidAsync(context.HttpContext); + + // Assert + Assert.False(result); + context.TokenGenerator.Verify(); + + // Failed _after_ updating the AntiforgeryContext. + Assert.NotNull(antiforgeryFeature); + Assert.True(antiforgeryFeature.HaveDeserializedCookieToken); + Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken); + Assert.True(antiforgeryFeature.HaveDeserializedRequestToken); + Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.RequestToken); + } + + [Fact] + public async Task IsRequestValidAsync_FromStore_Success() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature(); + var context = CreateMockContext(new AntiforgeryOptions(), antiforgeryFeature: antiforgeryFeature); + context.HttpContext.Request.Method = "POST"; + + string message; + context.TokenGenerator + .Setup(o => o.TryValidateTokenSet( + context.HttpContext, + context.TestTokenSet.OldCookieToken, + context.TestTokenSet.RequestToken, + out message)) + .Returns(true) + .Verifiable(); + + var antiforgery = GetAntiforgery(context); + + // Act + var result = await antiforgery.IsRequestValidAsync(context.HttpContext); + + // Assert + Assert.True(result); + context.TokenGenerator.Verify(); + + Assert.NotNull(antiforgeryFeature); + Assert.True(antiforgeryFeature.HaveDeserializedCookieToken); + Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken); + Assert.True(antiforgeryFeature.HaveDeserializedRequestToken); + Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.RequestToken); + } + + [Fact] + public async Task IsRequestValidAsync_DoesNotDeserializeTwice() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature + { + HaveDeserializedCookieToken = true, + CookieToken = new AntiforgeryToken(), + HaveDeserializedRequestToken = true, + RequestToken = new AntiforgeryToken(), + }; + var context = CreateMockContext(new AntiforgeryOptions(), antiforgeryFeature: antiforgeryFeature); + context.HttpContext.Request.Method = "POST"; + + string message; + context.TokenGenerator + .Setup(o => o.TryValidateTokenSet( + context.HttpContext, + antiforgeryFeature.CookieToken, + antiforgeryFeature.RequestToken, + out message)) + .Returns(true) + .Verifiable(); + + var antiforgery = GetAntiforgery(context); + + // Act + var result = await antiforgery.IsRequestValidAsync(context.HttpContext); + + // Assert + Assert.True(result); + context.TokenGenerator.Verify(); + + // Token serializer not used. + context.TokenSerializer.Verify( + o => o.Deserialize(It.IsAny()), + Times.Never); + context.TokenSerializer.Verify( + o => o.Serialize(It.IsAny()), + Times.Never); + } + + [Theory] + [InlineData("GeT")] + [InlineData("HEAD")] + [InlineData("options")] + [InlineData("TrAcE")] + public async Task IsRequestValidAsync_SkipsAntiforgery_ForSafeHttpMethods(string httpMethod) + { + // Arrange + var context = CreateMockContext(new AntiforgeryOptions()); + context.HttpContext.Request.Method = httpMethod; + + string message; + context.TokenGenerator + .Setup(o => o.TryValidateTokenSet( + context.HttpContext, + It.IsAny(), + It.IsAny(), + out message)) + .Returns(false) + .Verifiable(); + + var antiforgery = GetAntiforgery(context); + + // Act + var result = await antiforgery.IsRequestValidAsync(context.HttpContext); + + // Assert + Assert.True(result); + context.TokenGenerator + .Verify(o => o.TryValidateTokenSet( + context.HttpContext, + It.IsAny(), + It.IsAny(), + out message), + Times.Never); + } + + [Theory] + [InlineData("PUT")] + [InlineData("post")] + [InlineData("Delete")] + [InlineData("Custom")] + public async Task IsRequestValidAsync_ValidatesAntiforgery_ForNonSafeHttpMethods(string httpMethod) + { + // Arrange + var context = CreateMockContext(new AntiforgeryOptions()); + context.HttpContext.Request.Method = httpMethod; + + string message; + context.TokenGenerator + .Setup(o => o.TryValidateTokenSet( + context.HttpContext, + It.IsAny(), + It.IsAny(), + out message)) + .Returns(true) + .Verifiable(); + + var antiforgery = GetAntiforgery(context); + + // Act + var result = await antiforgery.IsRequestValidAsync(context.HttpContext); + + // Assert + Assert.True(result); + context.TokenGenerator.Verify(); + } + + [Fact] + public async Task ValidateRequestAsync_FromStore_Failure() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature(); + var context = CreateMockContext(new AntiforgeryOptions(), antiforgeryFeature: antiforgeryFeature); + + var message = "my-message"; + context.TokenGenerator + .Setup(o => o.TryValidateTokenSet( + context.HttpContext, + context.TestTokenSet.OldCookieToken, + context.TestTokenSet.RequestToken, + out message)) + .Returns(false) + .Verifiable(); + var antiforgery = GetAntiforgery(context); + + // Act & assert + var exception = await Assert.ThrowsAsync( + () => antiforgery.ValidateRequestAsync(context.HttpContext)); + Assert.Equal("my-message", exception.Message); + context.TokenGenerator.Verify(); + + // Failed _after_ updating the AntiforgeryContext. + Assert.NotNull(antiforgeryFeature); + Assert.True(antiforgeryFeature.HaveDeserializedCookieToken); + Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken); + Assert.True(antiforgeryFeature.HaveDeserializedRequestToken); + Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.RequestToken); + } + + [Fact] + public async Task ValidateRequestAsync_FromStore_Success() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature(); + var context = CreateMockContext(new AntiforgeryOptions(), antiforgeryFeature: antiforgeryFeature); + + string message; + context.TokenGenerator + .Setup(o => o.TryValidateTokenSet( + context.HttpContext, + context.TestTokenSet.OldCookieToken, + context.TestTokenSet.RequestToken, + out message)) + .Returns(true) + .Verifiable(); + + var antiforgery = GetAntiforgery(context); + + // Act + await antiforgery.ValidateRequestAsync(context.HttpContext); + + // Assert + context.TokenGenerator.Verify(); + + Assert.NotNull(antiforgeryFeature); + Assert.True(antiforgeryFeature.HaveDeserializedCookieToken); + Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken); + Assert.True(antiforgeryFeature.HaveDeserializedRequestToken); + Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.RequestToken); + } + + [Fact] + public async Task ValidateRequestAsync_NoCookieToken_Throws() + { + // Arrange + var context = CreateMockContext(new AntiforgeryOptions() + { + Cookie = { Name = "cookie-name" }, + FormFieldName = "form-field-name", + HeaderName = null, + }); + + var tokenSet = new AntiforgeryTokenSet(null, null, "form-field-name", null); + context.TokenStore + .Setup(s => s.GetRequestTokensAsync(context.HttpContext)) + .Returns(Task.FromResult(tokenSet)); + + var antiforgery = GetAntiforgery(context); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => antiforgery.ValidateRequestAsync(context.HttpContext)); + Assert.Equal("The required antiforgery cookie \"cookie-name\" is not present.", exception.Message); + } + + [Fact] + public async Task ValidateRequestAsync_NonFormRequest_HeaderDisabled_Throws() + { + // Arrange + var context = CreateMockContext(new AntiforgeryOptions() + { + Cookie = { Name = "cookie-name" }, + FormFieldName = "form-field-name", + HeaderName = null, + }); + + var tokenSet = new AntiforgeryTokenSet(null, "cookie-token", "form-field-name", null); + context.TokenStore + .Setup(s => s.GetRequestTokensAsync(context.HttpContext)) + .Returns(Task.FromResult(tokenSet)); + + var antiforgery = GetAntiforgery(context); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => antiforgery.ValidateRequestAsync(context.HttpContext)); + Assert.Equal("The required antiforgery form field \"form-field-name\" is not present.", exception.Message); + } + + [Fact] + public async Task ValidateRequestAsync_NonFormRequest_NoHeaderValue_Throws() + { + // Arrange + var context = CreateMockContext(new AntiforgeryOptions() + { + Cookie = { Name = "cookie-name" }, + FormFieldName = "form-field-name", + HeaderName = "header-name", + }); + + context.HttpContext.Request.ContentType = "application/json"; + + var tokenSet = new AntiforgeryTokenSet(null, "cookie-token", "form-field-name", "header-name"); + context.TokenStore + .Setup(s => s.GetRequestTokensAsync(context.HttpContext)) + .Returns(Task.FromResult(tokenSet)); + + var antiforgery = GetAntiforgery(context); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => antiforgery.ValidateRequestAsync(context.HttpContext)); + Assert.Equal("The required antiforgery header value \"header-name\" is not present.", exception.Message); + } + + [Fact] + public async Task ValidateRequestAsync_FormRequest_NoRequestTokenValue_Throws() + { + // Arrange + var context = CreateMockContext(new AntiforgeryOptions() + { + Cookie = { Name = "cookie-name" }, + FormFieldName = "form-field-name", + HeaderName = "header-name", + }); + + context.HttpContext.Request.ContentType = "application/x-www-form-urlencoded"; + + var tokenSet = new AntiforgeryTokenSet(null, "cookie-token", "form-field-name", "header-name"); + context.TokenStore + .Setup(s => s.GetRequestTokensAsync(context.HttpContext)) + .Returns(Task.FromResult(tokenSet)); + + var antiforgery = GetAntiforgery(context); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => antiforgery.ValidateRequestAsync(context.HttpContext)); + Assert.Equal( + "The required antiforgery request token was not provided in either form field \"form-field-name\" " + + "or header value \"header-name\".", + exception.Message); + } + + [Fact] + public async Task ValidateRequestAsync_DoesNotDeserializeTwice() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature + { + HaveDeserializedCookieToken = true, + CookieToken = new AntiforgeryToken(), + HaveDeserializedRequestToken = true, + RequestToken = new AntiforgeryToken(), + }; + var context = CreateMockContext(new AntiforgeryOptions(), antiforgeryFeature: antiforgeryFeature); + + string message; + context.TokenGenerator + .Setup(o => o.TryValidateTokenSet( + context.HttpContext, + antiforgeryFeature.CookieToken, + antiforgeryFeature.RequestToken, + out message)) + .Returns(true) + .Verifiable(); + + var antiforgery = GetAntiforgery(context); + + // Act + await antiforgery.ValidateRequestAsync(context.HttpContext); + + // Assert (does not throw) + context.TokenGenerator.Verify(); + + // Token serializer not used. + context.TokenSerializer.Verify( + o => o.Deserialize(It.IsAny()), + Times.Never); + context.TokenSerializer.Verify( + o => o.Serialize(It.IsAny()), + Times.Never); + } + + [Fact] + public void SetCookieTokenAndHeader_PreserveXFrameOptionsHeader() + { + // Arrange + var options = new AntiforgeryOptions(); + var antiforgeryFeature = new AntiforgeryFeature(); + var expectedHeaderValue = "DIFFERENTORIGIN"; + + // Generate a new cookie. + var context = CreateMockContext( + options, + useOldCookie: false, + isOldCookieValid: false, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + context.HttpContext.Response.Headers["X-Frame-Options"] = expectedHeaderValue; + + // Act + antiforgery.SetCookieTokenAndHeader(context.HttpContext); + + // Assert + var xFrameOptions = context.HttpContext.Response.Headers["X-Frame-Options"]; + Assert.Equal(expectedHeaderValue, xFrameOptions); + } + + [Fact] + public void SetCookieTokenAndHeader_NewCookieToken_SetsDoNotCacheHeaders() + { + // Arrange + var options = new AntiforgeryOptions(); + var antiforgeryFeature = new AntiforgeryFeature(); + + // Generate a new cookie. + var context = CreateMockContext( + options, + useOldCookie: false, + isOldCookieValid: false, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + + // Act + antiforgery.SetCookieTokenAndHeader(context.HttpContext); + + // Assert + Assert.Equal("no-cache, no-store", context.HttpContext.Response.Headers["Cache-Control"]); + Assert.Equal("no-cache", context.HttpContext.Response.Headers["Pragma"]); + } + + [Fact] + public void SetCookieTokenAndHeader_ValidOldCookieToken_SetsDoNotCacheHeaders() + { + // Arrange + var options = new AntiforgeryOptions(); + var antiforgeryFeature = new AntiforgeryFeature(); + + // Generate a new cookie. + var context = CreateMockContext( + options, + useOldCookie: true, + isOldCookieValid: true, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + + // Act + antiforgery.SetCookieTokenAndHeader(context.HttpContext); + + // Assert + Assert.Equal("no-cache, no-store", context.HttpContext.Response.Headers["Cache-Control"]); + Assert.Equal("no-cache", context.HttpContext.Response.Headers["Pragma"]); + } + + [Fact] + public void SetCookieTokenAndHeader_OverridesExistingCachingHeaders() + { + // Arrange + var options = new AntiforgeryOptions(); + var antiforgeryFeature = new AntiforgeryFeature(); + + // Generate a new cookie. + var context = CreateMockContext( + options, + useOldCookie: true, + isOldCookieValid: true, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + context.HttpContext.Response.Headers["Cache-Control"] = "public"; + + // Act + antiforgery.SetCookieTokenAndHeader(context.HttpContext); + + // Assert + Assert.Equal("no-cache, no-store", context.HttpContext.Response.Headers["Cache-Control"]); + Assert.Equal("no-cache", context.HttpContext.Response.Headers["Pragma"]); + } + + [Theory] + [InlineData(false, "SAMEORIGIN")] + [InlineData(true, null)] + public void SetCookieTokenAndHeader_AddsXFrameOptionsHeader( + bool suppressXFrameOptions, + string expectedHeaderValue) + { + // Arrange + var options = new AntiforgeryOptions() + { + SuppressXFrameOptionsHeader = suppressXFrameOptions + }; + var antiforgeryFeature = new AntiforgeryFeature(); + + // Generate a new cookie. + var context = CreateMockContext( + options, + useOldCookie: false, + isOldCookieValid: false, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + + // Act + antiforgery.SetCookieTokenAndHeader(context.HttpContext); + + // Assert + var xFrameOptions = context.HttpContext.Response.Headers["X-Frame-Options"]; + Assert.Equal(expectedHeaderValue, xFrameOptions); + + Assert.NotNull(antiforgeryFeature); + Assert.True(antiforgeryFeature.HaveDeserializedCookieToken); + Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken); + Assert.True(antiforgeryFeature.HaveGeneratedNewCookieToken); + Assert.Equal(context.TestTokenSet.NewCookieToken, antiforgeryFeature.NewCookieToken); + Assert.Equal(context.TestTokenSet.NewCookieTokenString, antiforgeryFeature.NewCookieTokenString); + Assert.True(antiforgeryFeature.HaveStoredNewCookieToken); + } + + [Fact] + public void SetCookieTokenAndHeader_DoesNotDeserializeTwice() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature + { + HaveDeserializedCookieToken = true, + HaveGeneratedNewCookieToken = true, + NewCookieToken = new AntiforgeryToken(), + NewCookieTokenString = "serialized-cookie-token-from-context", + NewRequestToken = new AntiforgeryToken(), + NewRequestTokenString = "serialized-form-token-from-context", + }; + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: true, + isOldCookieValid: true, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + + context.TokenStore + .Setup(t => t.SaveCookieToken(context.HttpContext, "serialized-cookie-token-from-context")) + .Verifiable(); + + // Act + antiforgery.SetCookieTokenAndHeader(context.HttpContext); + + // Assert + // Token store used once, with expected arguments. + // Passed context's cookie token though request's cookie token was valid. + context.TokenStore.Verify( + t => t.SaveCookieToken(context.HttpContext, "serialized-cookie-token-from-context"), + Times.Once); + + // Token serializer not used. + context.TokenSerializer.Verify( + o => o.Deserialize(It.IsAny()), + Times.Never); + context.TokenSerializer.Verify( + o => o.Serialize(It.IsAny()), + Times.Never); + } + + [Fact] + public void SetCookieTokenAndHeader_DoesNotStoreTwice() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature + { + HaveDeserializedCookieToken = true, + HaveGeneratedNewCookieToken = true, + HaveStoredNewCookieToken = true, + NewCookieToken = new AntiforgeryToken(), + NewCookieTokenString = "serialized-cookie-token-from-context", + NewRequestToken = new AntiforgeryToken(), + NewRequestTokenString = "serialized-form-token-from-context", + }; + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: true, + isOldCookieValid: true, + antiforgeryFeature: antiforgeryFeature); + var antiforgery = GetAntiforgery(context); + + // Act + antiforgery.SetCookieTokenAndHeader(context.HttpContext); + + // Assert + // Token serializer not used. + context.TokenSerializer.Verify( + o => o.Deserialize(It.IsAny()), + Times.Never); + context.TokenSerializer.Verify( + o => o.Serialize(It.IsAny()), + Times.Never); + + // Token store not used. + context.TokenStore.Verify( + t => t.SaveCookieToken(It.IsAny(), It.IsAny()), + Times.Never); + } + + [Fact] + public void SetCookieTokenAndHeader_NullCookieToken() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature + { + HaveDeserializedCookieToken = false, + HaveGeneratedNewCookieToken = false, + HaveStoredNewCookieToken = true, + NewCookieToken = new AntiforgeryToken(), + NewCookieTokenString = "serialized-cookie-token-from-context", + NewRequestToken = new AntiforgeryToken(), + NewRequestTokenString = "serialized-form-token-from-context", + }; + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: false, + isOldCookieValid: false, + antiforgeryFeature: antiforgeryFeature); + var testTokenSet = new TestTokenSet + { + OldCookieTokenString = null + }; + + var nullTokenStore = GetTokenStore(context.HttpContext, testTokenSet, false); + var antiforgery = GetAntiforgery( + context.HttpContext, + tokenGenerator: context.TokenGenerator.Object, + tokenStore: nullTokenStore.Object); + + // Act + antiforgery.SetCookieTokenAndHeader(context.HttpContext); + + // Assert + context.TokenSerializer.Verify(s => s.Deserialize(null), Times.Never); + } + + [Fact] + public void SetCookieTokenAndHeader_DoesNotModifyHeadersAfterResponseHasStarted() + { + // Arrange + var antiforgeryFeature = new AntiforgeryFeature + { + HaveDeserializedCookieToken = false, + HaveGeneratedNewCookieToken = false, + HaveStoredNewCookieToken = true, + NewCookieToken = new AntiforgeryToken(), + NewCookieTokenString = "serialized-cookie-token-from-context", + NewRequestToken = new AntiforgeryToken(), + NewRequestTokenString = "serialized-form-token-from-context", + }; + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: false, + isOldCookieValid: false, + antiforgeryFeature: antiforgeryFeature); + var testTokenSet = new TestTokenSet + { + OldCookieTokenString = null + }; + + var nullTokenStore = GetTokenStore(context.HttpContext, testTokenSet, false); + var antiforgery = GetAntiforgery( + context.HttpContext, + tokenGenerator: context.TokenGenerator.Object, + tokenStore: nullTokenStore.Object); + + TestResponseFeature testResponse = new TestResponseFeature(); + context.HttpContext.Features.Set(testResponse); + context.HttpContext.Response.Headers["Cache-Control"] = "public"; + testResponse.StartResponse(); + + // Act + antiforgery.SetCookieTokenAndHeader(context.HttpContext); + + Assert.Equal("public", context.HttpContext.Response.Headers["Cache-Control"]); + } + + [Fact] + public void GetAndStoreTokens_DoesNotLogWarning_IfNoExistingCacheHeadersPresent() + { + // Arrange + var testSink = new TestSink(); + var loggerFactory = new Mock(); + loggerFactory + .Setup(lf => lf.CreateLogger(typeof(DefaultAntiforgery).FullName)) + .Returns(new TestLogger("test logger", testSink, enabled: true)); + var services = new ServiceCollection(); + services.AddSingleton(loggerFactory.Object); + var antiforgeryFeature = new AntiforgeryFeature(); + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: false, + isOldCookieValid: false, + antiforgeryFeature: antiforgeryFeature); + context.HttpContext.RequestServices = services.BuildServiceProvider(); + var antiforgery = GetAntiforgery(context); + + // Act + var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext); + + // Assert + var hasWarningMessage = testSink.Writes + .Where(wc => wc.LogLevel == LogLevel.Warning) + .Select(wc => wc.State?.ToString()) + .Contains(ResponseCacheHeadersOverrideWarningMessage); + Assert.False(hasWarningMessage); + } + + [Theory] + [InlineData("Cache-Control", "Public")] + [InlineData("Cache-Control", "PuBlic")] + [InlineData("Cache-Control", "Private")] + [InlineData("Cache-Control", "PriVate")] + [InlineData("Cache-Control", "No-Store")] + [InlineData("Cache-Control", "No-store")] + [InlineData("Pragma", "Foo")] + public void GetAndStoreTokens_LogsWarning_NonNoCacheHeadersAlreadyPresent(string headerName, string headerValue) + { + // Arrange + var testSink = new TestSink(); + var loggerFactory = new Mock(); + loggerFactory + .Setup(lf => lf.CreateLogger(typeof(DefaultAntiforgery).FullName)) + .Returns(new TestLogger("test logger", testSink, enabled: true)); + var services = new ServiceCollection(); + services.AddSingleton(loggerFactory.Object); + var antiforgeryFeature = new AntiforgeryFeature(); + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: false, + isOldCookieValid: false, + antiforgeryFeature: antiforgeryFeature); + context.HttpContext.RequestServices = services.BuildServiceProvider(); + var antiforgery = GetAntiforgery(context); + context.HttpContext.Response.Headers[headerName] = headerValue; + + // Act + var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext); + + // Assert + var hasWarningMessage = testSink.Writes + .Where(wc => wc.LogLevel == LogLevel.Warning) + .Select(wc => wc.State?.ToString()) + .Contains(ResponseCacheHeadersOverrideWarningMessage); + Assert.True(hasWarningMessage); + } + + [Theory] + [InlineData("Cache-Control", "no-cache")] + [InlineData("Pragma", "no-cache")] + public void GetAndStoreTokens_DoesNotLogsWarning_ForNoCacheHeaders_AlreadyPresent(string headerName, string headerValue) + { + // Arrange + var testSink = new TestSink(); + var loggerFactory = new Mock(); + loggerFactory + .Setup(lf => lf.CreateLogger(typeof(DefaultAntiforgery).FullName)) + .Returns(new TestLogger("test logger", testSink, enabled: true)); + var services = new ServiceCollection(); + services.AddSingleton(loggerFactory.Object); + var antiforgeryFeature = new AntiforgeryFeature(); + var context = CreateMockContext( + new AntiforgeryOptions(), + useOldCookie: false, + isOldCookieValid: false, + antiforgeryFeature: antiforgeryFeature); + context.HttpContext.RequestServices = services.BuildServiceProvider(); + var antiforgery = GetAntiforgery(context); + context.HttpContext.Response.Headers[headerName] = headerValue; + + // Act + var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext); + + // Assert + var hasWarningMessage = testSink.Writes + .Where(wc => wc.LogLevel == LogLevel.Warning) + .Select(wc => wc.State?.ToString()) + .Contains(ResponseCacheHeadersOverrideWarningMessage); + Assert.False(hasWarningMessage); + } + + private DefaultAntiforgery GetAntiforgery( + HttpContext httpContext, + AntiforgeryOptions options = null, + IAntiforgeryTokenGenerator tokenGenerator = null, + IAntiforgeryTokenSerializer tokenSerializer = null, + IAntiforgeryTokenStore tokenStore = null) + { + var optionsManager = new TestOptionsManager(); + if (options != null) + { + optionsManager.Value = options; + } + + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + return new DefaultAntiforgery( + antiforgeryOptionsAccessor: optionsManager, + tokenGenerator: tokenGenerator, + tokenSerializer: tokenSerializer, + tokenStore: tokenStore, + loggerFactory: loggerFactory); + } + + private IServiceProvider GetServices() + { + var builder = new ServiceCollection(); + builder.AddSingleton(new LoggerFactory()); + + return builder.BuildServiceProvider(); + } + + private HttpContext GetHttpContext(IAntiforgeryFeature antiforgeryFeature = null) + { + var httpContext = new DefaultHttpContext(); + antiforgeryFeature = antiforgeryFeature ?? new AntiforgeryFeature(); + httpContext.Features.Set(antiforgeryFeature); + httpContext.RequestServices = GetServices(); + httpContext.User = new ClaimsPrincipal(new ClaimsIdentity("some-auth")); + + return httpContext; + } + + private DefaultAntiforgery GetAntiforgery(AntiforgeryMockContext context) + { + return GetAntiforgery( + context.HttpContext, + context.Options, + context.TokenGenerator?.Object, + context.TokenSerializer?.Object, + context.TokenStore?.Object); + } + + private Mock GetTokenStore( + HttpContext context, + TestTokenSet testTokenSet, + bool saveNewCookie = true) + { + var oldCookieToken = testTokenSet.OldCookieTokenString; + var formToken = testTokenSet.FormTokenString; + var mockTokenStore = new Mock(MockBehavior.Strict); + mockTokenStore + .Setup(o => o.GetCookieToken(context)) + .Returns(oldCookieToken); + + mockTokenStore + .Setup(o => o.GetRequestTokensAsync(context)) + .Returns(() => Task.FromResult(new AntiforgeryTokenSet( + formToken, + oldCookieToken, + "form", + "header"))); + + if (saveNewCookie) + { + var newCookieToken = testTokenSet.NewCookieTokenString; + mockTokenStore + .Setup(o => o.SaveCookieToken(context, newCookieToken)) + .Verifiable(); + } + + return mockTokenStore; + } + + private Mock GetTokenSerializer(TestTokenSet testTokenSet) + { + var oldCookieToken = testTokenSet.OldCookieToken; + var newCookieToken = testTokenSet.NewCookieToken; + var formToken = testTokenSet.RequestToken; + var mockSerializer = new Mock(MockBehavior.Strict); + mockSerializer.Setup(o => o.Serialize(formToken)) + .Returns(testTokenSet.FormTokenString); + mockSerializer.Setup(o => o.Deserialize(testTokenSet.FormTokenString)) + .Returns(formToken); + mockSerializer.Setup(o => o.Deserialize(testTokenSet.OldCookieTokenString)) + .Returns(oldCookieToken); + mockSerializer.Setup(o => o.Serialize(oldCookieToken)) + .Returns(testTokenSet.OldCookieTokenString); + mockSerializer.Setup(o => o.Serialize(newCookieToken)) + .Returns(testTokenSet.NewCookieTokenString); + return mockSerializer; + } + + private AntiforgeryMockContext CreateMockContext( + AntiforgeryOptions options, + bool useOldCookie = false, + bool isOldCookieValid = true, + IAntiforgeryFeature antiforgeryFeature = null) + { + // Arrange + var httpContext = GetHttpContext(antiforgeryFeature); + var testTokenSet = GetTokenSet(); + + var mockSerializer = GetTokenSerializer(testTokenSet); + + var mockTokenStore = GetTokenStore(httpContext, testTokenSet, !useOldCookie); + + var mockGenerator = new Mock(MockBehavior.Strict); + mockGenerator + .Setup(o => o.GenerateRequestToken( + httpContext, + useOldCookie ? testTokenSet.OldCookieToken : testTokenSet.NewCookieToken)) + .Returns(testTokenSet.RequestToken); + + mockGenerator + .Setup(o => o.GenerateCookieToken()) + .Returns(useOldCookie ? testTokenSet.OldCookieToken : testTokenSet.NewCookieToken); + mockGenerator + .Setup(o => o.IsCookieTokenValid(null)) + .Returns(false); + mockGenerator + .Setup(o => o.IsCookieTokenValid(testTokenSet.OldCookieToken)) + .Returns(isOldCookieValid); + + mockGenerator + .Setup(o => o.IsCookieTokenValid(testTokenSet.NewCookieToken)) + .Returns(!isOldCookieValid); + + return new AntiforgeryMockContext() + { + Options = options, + HttpContext = httpContext, + TokenGenerator = mockGenerator, + TokenSerializer = mockSerializer, + TokenStore = mockTokenStore, + TestTokenSet = testTokenSet + }; + } + + private TestTokenSet GetTokenSet() + { + return new TestTokenSet() + { + RequestToken = new AntiforgeryToken() { IsCookieToken = false }, + FormTokenString = "serialized-form-token", + OldCookieToken = new AntiforgeryToken() { IsCookieToken = true }, + OldCookieTokenString = "serialized-old-cookie-token", + NewCookieToken = new AntiforgeryToken() { IsCookieToken = true }, + NewCookieTokenString = "serialized-new-cookie-token", + }; + } + + private class TestTokenSet + { + public AntiforgeryToken RequestToken { get; set; } + + public string FormTokenString { get; set; } + + public AntiforgeryToken OldCookieToken { get; set; } + + public string OldCookieTokenString { get; set; } + + public AntiforgeryToken NewCookieToken { get; set; } + + public string NewCookieTokenString { get; set; } + } + + private class AntiforgeryMockContext + { + public AntiforgeryOptions Options { get; set; } + + public TestTokenSet TestTokenSet { get; set; } + + public HttpContext HttpContext { get; set; } + + public Mock TokenGenerator { get; set; } + + public Mock TokenStore { get; set; } + + public Mock TokenSerializer { get; set; } + } + + private class TestOptionsManager : IOptions + { + public AntiforgeryOptions Value { get; set; } = new AntiforgeryOptions(); + } + + private class TestResponseFeature : HttpResponseFeature + { + private bool _hasStarted = false; + + public override bool HasStarted { get => _hasStarted; } + + public TestResponseFeature() + { + } + + public void StartResponse() + { + _hasStarted = true; + } + } + } +} diff --git a/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenGeneratorTest.cs b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenGeneratorTest.cs new file mode 100644 index 0000000000..981de8e94c --- /dev/null +++ b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenGeneratorTest.cs @@ -0,0 +1,628 @@ +// 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 System; +using System.Security.Claims; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class DefaultAntiforgeryTokenGeneratorProviderTest + { + [Fact] + public void GenerateCookieToken() + { + // Arrange + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: null, + additionalDataProvider: null); + + // Act + var token = tokenProvider.GenerateCookieToken(); + + // Assert + Assert.NotNull(token); + } + + [Fact] + public void GenerateRequestToken_InvalidCookieToken() + { + // Arrange + var cookieToken = new AntiforgeryToken() { IsCookieToken = false }; + var httpContext = new DefaultHttpContext(); + httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); + Assert.False(httpContext.User.Identity.IsAuthenticated); + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: null, + additionalDataProvider: null); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => tokenProvider.GenerateRequestToken(httpContext, cookieToken), + "cookieToken", + "The antiforgery cookie token is invalid."); + } + + [Fact] + public void GenerateRequestToken_AnonymousUser() + { + // Arrange + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + var httpContext = new DefaultHttpContext(); + httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); + Assert.False(httpContext.User.Identity.IsAuthenticated); + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: null, + additionalDataProvider: null); + + // Act + var fieldToken = tokenProvider.GenerateRequestToken(httpContext, cookieToken); + + // Assert + Assert.NotNull(fieldToken); + Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); + Assert.False(fieldToken.IsCookieToken); + Assert.Empty(fieldToken.Username); + Assert.Null(fieldToken.ClaimUid); + Assert.Empty(fieldToken.AdditionalData); + } + + [Fact] + public void GenerateRequestToken_AuthenticatedWithoutUsernameAndNoAdditionalData_NoAdditionalData() + { + // Arrange + var cookieToken = new AntiforgeryToken() + { + IsCookieToken = true + }; + + var httpContext = new DefaultHttpContext(); + httpContext.User = new ClaimsPrincipal(new MyAuthenticatedIdentityWithoutUsername()); + + var options = new AntiforgeryOptions(); + var claimUidExtractor = new Mock().Object; + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: claimUidExtractor, + additionalDataProvider: null); + + // Act & assert + var exception = Assert.Throws( + () => tokenProvider.GenerateRequestToken(httpContext, cookieToken)); + Assert.Equal( + "The provided identity of type " + + $"'{typeof(MyAuthenticatedIdentityWithoutUsername).FullName}' " + + "is marked IsAuthenticated = true but does not have a value for Name. " + + "By default, the antiforgery system requires that all authenticated identities have a unique Name. " + + "If it is not possible to provide a unique Name for this identity, " + + "consider extending IAntiforgeryAdditionalDataProvider by overriding the " + + "DefaultAntiforgeryAdditionalDataProvider " + + "or a custom type that can provide some form of unique identifier for the current user.", + exception.Message); + } + + [Fact] + public void GenerateRequestToken_AuthenticatedWithoutUsername_WithAdditionalData() + { + // Arrange + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + + var httpContext = new DefaultHttpContext(); + httpContext.User = new ClaimsPrincipal(new MyAuthenticatedIdentityWithoutUsername()); + + var mockAdditionalDataProvider = new Mock(); + mockAdditionalDataProvider.Setup(o => o.GetAdditionalData(httpContext)) + .Returns("additional-data"); + + var claimUidExtractor = new Mock().Object; + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: claimUidExtractor, + additionalDataProvider: mockAdditionalDataProvider.Object); + + // Act + var fieldToken = tokenProvider.GenerateRequestToken(httpContext, cookieToken); + + // Assert + Assert.NotNull(fieldToken); + Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); + Assert.False(fieldToken.IsCookieToken); + Assert.Empty(fieldToken.Username); + Assert.Null(fieldToken.ClaimUid); + Assert.Equal("additional-data", fieldToken.AdditionalData); + } + + [Fact] + public void GenerateRequestToken_ClaimsBasedIdentity() + { + // Arrange + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + + var identity = GetAuthenticatedIdentity("some-identity"); + var httpContext = new DefaultHttpContext(); + httpContext.User = new ClaimsPrincipal(identity); + + byte[] data = new byte[256 / 8]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(data); + } + var base64ClaimUId = Convert.ToBase64String(data); + var expectedClaimUid = new BinaryBlob(256, data); + + var mockClaimUidExtractor = new Mock(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.Is(c => c.Identity == identity))) + .Returns(base64ClaimUId); + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: mockClaimUidExtractor.Object, + additionalDataProvider: null); + + // Act + var fieldToken = tokenProvider.GenerateRequestToken(httpContext, cookieToken); + + // Assert + Assert.NotNull(fieldToken); + Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); + Assert.False(fieldToken.IsCookieToken); + Assert.Equal("", fieldToken.Username); + Assert.Equal(expectedClaimUid, fieldToken.ClaimUid); + Assert.Equal("", fieldToken.AdditionalData); + } + + [Fact] + public void GenerateRequestToken_RegularUserWithUsername() + { + // Arrange + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + + var httpContext = new DefaultHttpContext(); + var mockIdentity = new Mock(); + mockIdentity.Setup(o => o.IsAuthenticated) + .Returns(true); + mockIdentity.Setup(o => o.Name) + .Returns("my-username"); + + httpContext.User = new ClaimsPrincipal(mockIdentity.Object); + + var claimUidExtractor = new Mock().Object; + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: claimUidExtractor, + additionalDataProvider: null); + + // Act + var fieldToken = tokenProvider.GenerateRequestToken(httpContext, cookieToken); + + // Assert + Assert.NotNull(fieldToken); + Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); + Assert.False(fieldToken.IsCookieToken); + Assert.Equal("my-username", fieldToken.Username); + Assert.Null(fieldToken.ClaimUid); + Assert.Empty(fieldToken.AdditionalData); + } + + [Fact] + public void IsCookieTokenValid_FieldToken_ReturnsFalse() + { + // Arrange + var cookieToken = new AntiforgeryToken() + { + IsCookieToken = false + }; + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: null, + additionalDataProvider: null); + + // Act + var isValid = tokenProvider.IsCookieTokenValid(cookieToken); + + // Assert + Assert.False(isValid); + } + + [Fact] + public void IsCookieTokenValid_NullToken_ReturnsFalse() + { + // Arrange + AntiforgeryToken cookieToken = null; + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: null, + additionalDataProvider: null); + + // Act + var isValid = tokenProvider.IsCookieTokenValid(cookieToken); + + // Assert + Assert.False(isValid); + } + + [Fact] + public void IsCookieTokenValid_ValidToken_ReturnsTrue() + { + // Arrange + var cookieToken = new AntiforgeryToken() + { + IsCookieToken = true + }; + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: null, + additionalDataProvider: null); + + // Act + var isValid = tokenProvider.IsCookieTokenValid(cookieToken); + + // Assert + Assert.True(isValid); + } + + + [Fact] + public void TryValidateTokenSet_CookieTokenMissing() + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); + + var fieldtoken = new AntiforgeryToken() { IsCookieToken = false }; + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: null, + additionalDataProvider: null); + + // Act & Assert + string message; + var ex = Assert.Throws( + () => tokenProvider.TryValidateTokenSet(httpContext, null, fieldtoken, out message)); + + var trimmed = ex.Message.Substring(0, ex.Message.IndexOf(Environment.NewLine)); + Assert.Equal(@"The required antiforgery cookie token must be provided.", trimmed); + } + + [Fact] + public void TryValidateTokenSet_FieldTokenMissing() + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); + + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: null, + additionalDataProvider: null); + + + // Act & Assert + string message; + var ex = Assert.Throws( + () => tokenProvider.TryValidateTokenSet(httpContext, cookieToken, null, out message)); + + var trimmed = ex.Message.Substring(0, ex.Message.IndexOf(Environment.NewLine)); + Assert.Equal("The required antiforgery request token must be provided.", trimmed); + } + + [Fact] + public void TryValidateTokenSet_FieldAndCookieTokensSwapped_FieldTokenDuplicated() + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); + + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + var fieldtoken = new AntiforgeryToken() { IsCookieToken = false }; + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: null, + additionalDataProvider: null); + + string expectedMessage = + "Validation of the provided antiforgery token failed. " + + "The cookie token and the request token were swapped."; + + // Act + string message; + var result = tokenProvider.TryValidateTokenSet(httpContext, fieldtoken, fieldtoken, out message); + + // Assert + Assert.False(result); + Assert.Equal(expectedMessage, message); + } + + [Fact] + public void TryValidateTokenSet_FieldAndCookieTokensSwapped_CookieDuplicated() + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); + + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + var fieldtoken = new AntiforgeryToken() { IsCookieToken = false }; + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: null, + additionalDataProvider: null); + + string expectedMessage = + "Validation of the provided antiforgery token failed. " + + "The cookie token and the request token were swapped."; + + // Act + string message; + var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, cookieToken, out message); + + // Assert + Assert.False(result); + Assert.Equal(expectedMessage, message); + } + + [Fact] + public void TryValidateTokenSet_FieldAndCookieTokensHaveDifferentSecurityKeys() + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); + + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + var fieldtoken = new AntiforgeryToken() { IsCookieToken = false }; + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: null, + additionalDataProvider: null); + + string expectedMessage = "The antiforgery cookie token and request token do not match."; + + // Act + string message; + var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + + // Assert + Assert.False(result); + Assert.Equal(expectedMessage, message); + } + + [Theory] + [InlineData("the-user", "the-other-user")] + [InlineData("http://example.com/uri-casing", "http://example.com/URI-casing")] + [InlineData("https://example.com/secure-uri-casing", "https://example.com/secure-URI-casing")] + public void TryValidateTokenSet_UsernameMismatch(string identityUsername, string embeddedUsername) + { + // Arrange + var httpContext = new DefaultHttpContext(); + var identity = GetAuthenticatedIdentity(identityUsername); + httpContext.User = new ClaimsPrincipal(identity); + + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = cookieToken.SecurityToken, + Username = embeddedUsername, + IsCookieToken = false + }; + + var mockClaimUidExtractor = new Mock(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.Is(c => c.Identity == identity))) + .Returns((string)null); + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: mockClaimUidExtractor.Object, + additionalDataProvider: null); + + string expectedMessage = + $"The provided antiforgery token was meant for user \"{embeddedUsername}\", " + + $"but the current user is \"{identityUsername}\"."; + + // Act + string message; + var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + + // Assert + Assert.False(result); + Assert.Equal(expectedMessage, message); + } + + [Fact] + public void TryValidateTokenSet_ClaimUidMismatch() + { + // Arrange + var httpContext = new DefaultHttpContext(); + var identity = GetAuthenticatedIdentity("the-user"); + httpContext.User = new ClaimsPrincipal(identity); + + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = cookieToken.SecurityToken, + IsCookieToken = false, + ClaimUid = new BinaryBlob(256) + }; + + var differentToken = new BinaryBlob(256); + var mockClaimUidExtractor = new Mock(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.Is(c => c.Identity == identity))) + .Returns(Convert.ToBase64String(differentToken.GetData())); + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: mockClaimUidExtractor.Object, + additionalDataProvider: null); + + string expectedMessage = + "The provided antiforgery token was meant for a different " + + "claims-based user than the current user."; + + // Act + string message; + var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + + // Assert + Assert.False(result); + Assert.Equal(expectedMessage, message); + } + + [Fact] + public void TryValidateTokenSet_AdditionalDataRejected() + { + // Arrange + var httpContext = new DefaultHttpContext(); + var identity = new ClaimsIdentity(); + httpContext.User = new ClaimsPrincipal(identity); + + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = cookieToken.SecurityToken, + Username = String.Empty, + IsCookieToken = false, + AdditionalData = "some-additional-data" + }; + + var mockAdditionalDataProvider = new Mock(); + mockAdditionalDataProvider + .Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data")) + .Returns(false); + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: null, + additionalDataProvider: mockAdditionalDataProvider.Object); + + string expectedMessage = "The provided antiforgery token failed a custom data check."; + + // Act + string message; + var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + + // Assert + Assert.False(result); + Assert.Equal(expectedMessage, message); + } + + [Fact] + public void TryValidateTokenSet_Success_AnonymousUser() + { + // Arrange + var httpContext = new DefaultHttpContext(); + var identity = new ClaimsIdentity(); + httpContext.User = new ClaimsPrincipal(identity); + + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = cookieToken.SecurityToken, + Username = String.Empty, + IsCookieToken = false, + AdditionalData = "some-additional-data" + }; + + var mockAdditionalDataProvider = new Mock(); + mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data")) + .Returns(true); + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: null, + additionalDataProvider: mockAdditionalDataProvider.Object); + + // Act + string message; + var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + + // Assert + Assert.True(result); + Assert.Null(message); + } + + [Fact] + public void TryValidateTokenSet_Success_AuthenticatedUserWithUsername() + { + // Arrange + var httpContext = new DefaultHttpContext(); + var identity = GetAuthenticatedIdentity("the-user"); + httpContext.User = new ClaimsPrincipal(identity); + + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = cookieToken.SecurityToken, + Username = "THE-USER", + IsCookieToken = false, + AdditionalData = "some-additional-data" + }; + + var mockAdditionalDataProvider = new Mock(); + mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data")) + .Returns(true); + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: new Mock().Object, + additionalDataProvider: mockAdditionalDataProvider.Object); + + // Act + string message; + var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + + // Assert + Assert.True(result); + Assert.Null(message); + } + + [Fact] + public void TryValidateTokenSet_Success_ClaimsBasedUser() + { + // Arrange + var httpContext = new DefaultHttpContext(); + var identity = GetAuthenticatedIdentity("the-user"); + httpContext.User = new ClaimsPrincipal(identity); + + var cookieToken = new AntiforgeryToken() { IsCookieToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = cookieToken.SecurityToken, + IsCookieToken = false, + ClaimUid = new BinaryBlob(256) + }; + + var mockClaimUidExtractor = new Mock(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.Is(c => c.Identity == identity))) + .Returns(Convert.ToBase64String(fieldtoken.ClaimUid.GetData())); + + var tokenProvider = new DefaultAntiforgeryTokenGenerator( + claimUidExtractor: mockClaimUidExtractor.Object, + additionalDataProvider: null); + + // Act + string message; + var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message); + + // Assert + Assert.True(result); + Assert.Null(message); + } + + private static ClaimsIdentity GetAuthenticatedIdentity(string identityUsername) + { + var claim = new Claim(ClaimsIdentity.DefaultNameClaimType, identityUsername); + return new ClaimsIdentity(new[] { claim }, "Some-Authentication"); + } + + private sealed class MyAuthenticatedIdentityWithoutUsername : ClaimsIdentity + { + public override bool IsAuthenticated + { + get { return true; } + } + + public override string Name + { + get { return String.Empty; } + } + } + } +} \ No newline at end of file diff --git a/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenSerializerTest.cs b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenSerializerTest.cs new file mode 100644 index 0000000000..88ce09b4e2 --- /dev/null +++ b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenSerializerTest.cs @@ -0,0 +1,189 @@ +// 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 System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.ObjectPool; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class DefaultAntiforgeryTokenSerializerTest + { + private static readonly Mock _dataProtector = GetDataProtector(); + private static readonly BinaryBlob _claimUid = new BinaryBlob(256, new byte[] { 0x6F, 0x16, 0x48, 0xE9, 0x72, 0x49, 0xAA, 0x58, 0x75, 0x40, 0x36, 0xA6, 0x7E, 0x24, 0x8C, 0xF0, 0x44, 0xF0, 0x7E, 0xCF, 0xB0, 0xED, 0x38, 0x75, 0x56, 0xCE, 0x02, 0x9A, 0x4F, 0x9A, 0x40, 0xE0 }); + private static readonly BinaryBlob _securityToken = new BinaryBlob(128, new byte[] { 0x70, 0x5E, 0xED, 0xCC, 0x7D, 0x42, 0xF1, 0xD6, 0xB3, 0xB9, 0x8A, 0x59, 0x36, 0x25, 0xBB, 0x4C }); + private static readonly ObjectPool _pool = + new DefaultObjectPoolProvider().Create(new AntiforgerySerializationContextPooledObjectPolicy()); + private const byte _salt = 0x05; + + [Theory] + [InlineData( + "01" // Version + + "705EEDCC7D42F1D6B3B9" // SecurityToken + // (WRONG!) Stream ends too early + )] + [InlineData( + "01" // Version + + "705EEDCC7D42F1D6B3B98A593625BB4C" // SecurityToken + + "01" // IsCookieToken + + "00" // (WRONG!) Too much data in stream + )] + [InlineData( + "02" // (WRONG! - must be 0x01) Version + + "705EEDCC7D42F1D6B3B98A593625BB4C" // SecurityToken + + "01" // IsCookieToken + )] + [InlineData( + "01" // Version + + "705EEDCC7D42F1D6B3B98A593625BB4C" // SecurityToken + + "00" // IsCookieToken + + "00" // IsClaimsBased + + "05" // Username length header + + "0000" // (WRONG!) Too little data in stream + )] + public void Deserialize_BadToken_Throws(string serializedToken) + { + // Arrange + var testSerializer = new DefaultAntiforgeryTokenSerializer(_dataProtector.Object, _pool); + + // Act & assert + var ex = Assert.Throws(() => testSerializer.Deserialize(serializedToken)); + Assert.Equal(@"The antiforgery token could not be decrypted.", ex.Message); + } + + [Fact] + public void Serialize_FieldToken_WithClaimUid_TokenRoundTripSuccessful() + { + // Arrange + var testSerializer = new DefaultAntiforgeryTokenSerializer(_dataProtector.Object, _pool); + + //"01" // Version + //+ "705EEDCC7D42F1D6B3B98A593625BB4C" // SecurityToken + //+ "00" // IsCookieToken + //+ "01" // IsClaimsBased + //+ "6F1648E97249AA58754036A67E248CF044F07ECFB0ED387556CE029A4F9A40E0" // ClaimUid + //+ "05" // AdditionalData length header + //+ "E282AC3437"; // AdditionalData ("€47") as UTF8 + var token = new AntiforgeryToken() + { + SecurityToken = _securityToken, + IsCookieToken = false, + ClaimUid = _claimUid, + AdditionalData = "€47" + }; + + // Act + var actualSerializedData = testSerializer.Serialize(token); + var deserializedToken = testSerializer.Deserialize(actualSerializedData); + + // Assert + AssertTokensEqual(token, deserializedToken); + _dataProtector.Verify(); + } + + [Fact] + public void Serialize_FieldToken_WithUsername_TokenRoundTripSuccessful() + { + // Arrange + var testSerializer = new DefaultAntiforgeryTokenSerializer(_dataProtector.Object, _pool); + + //"01" // Version + //+ "705EEDCC7D42F1D6B3B98A593625BB4C" // SecurityToken + //+ "00" // IsCookieToken + //+ "00" // IsClaimsBased + //+ "08" // Username length header + //+ "4AC3A972C3B46D65" // Username ("Jérôme") as UTF8 + //+ "05" // AdditionalData length header + //+ "E282AC3437"; // AdditionalData ("€47") as UTF8 + var token = new AntiforgeryToken() + { + SecurityToken = _securityToken, + IsCookieToken = false, + Username = "Jérôme", + AdditionalData = "€47" + }; + + // Act + var actualSerializedData = testSerializer.Serialize(token); + var deserializedToken = testSerializer.Deserialize(actualSerializedData); + + // Assert + AssertTokensEqual(token, deserializedToken); + _dataProtector.Verify(); + } + + [Fact] + public void Serialize_CookieToken_TokenRoundTripSuccessful() + { + // Arrange + var testSerializer = new DefaultAntiforgeryTokenSerializer(_dataProtector.Object, _pool); + + //"01" // Version + //+ "705EEDCC7D42F1D6B3B98A593625BB4C" // SecurityToken + //+ "01"; // IsCookieToken + var token = new AntiforgeryToken() + { + SecurityToken = _securityToken, + IsCookieToken = true + }; + + // Act + string actualSerializedData = testSerializer.Serialize(token); + var deserializedToken = testSerializer.Deserialize(actualSerializedData); + + // Assert + AssertTokensEqual(token, deserializedToken); + _dataProtector.Verify(); + } + + private static Mock GetDataProtector() + { + var mockCryptoSystem = new Mock(); + mockCryptoSystem.Setup(o => o.Protect(It.IsAny())) + .Returns(Protect) + .Verifiable(); + mockCryptoSystem.Setup(o => o.Unprotect(It.IsAny())) + .Returns(UnProtect) + .Verifiable(); + + var provider = new Mock(); + provider + .Setup(p => p.CreateProtector(It.IsAny())) + .Returns(mockCryptoSystem.Object); + return provider; + } + + private static byte[] Protect(byte[] data) + { + var input = new List(data); + input.Add(_salt); + return input.ToArray(); + } + + private static byte[] UnProtect(byte[] data) + { + var salt = data[data.Length - 1]; + if (salt != _salt) + { + throw new ArgumentException("Invalid salt value in data"); + } + + return data.Take(data.Length - 1).ToArray(); + } + + private static void AssertTokensEqual(AntiforgeryToken expected, AntiforgeryToken actual) + { + Assert.NotNull(expected); + Assert.NotNull(actual); + Assert.Equal(expected.AdditionalData, actual.AdditionalData); + Assert.Equal(expected.ClaimUid, actual.ClaimUid); + Assert.Equal(expected.IsCookieToken, actual.IsCookieToken); + Assert.Equal(expected.SecurityToken, actual.SecurityToken); + Assert.Equal(expected.Username, actual.Username); + } + } +} diff --git a/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenStoreTest.cs b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenStoreTest.cs new file mode 100644 index 0000000000..1ca1f57fc5 --- /dev/null +++ b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenStoreTest.cs @@ -0,0 +1,457 @@ +// 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 System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Primitives; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class DefaultAntiforgeryTokenStoreTest + { + private readonly string _cookieName = "cookie-name"; + + [Fact] + public void GetCookieToken_CookieDoesNotExist_ReturnsNull() + { + // Arrange + var httpContext = GetHttpContext(new RequestCookieCollection()); + var options = new AntiforgeryOptions + { + Cookie = { Name = _cookieName } + }; + + var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options)); + + // Act + var token = tokenStore.GetCookieToken(httpContext); + + // Assert + Assert.Null(token); + } + + [Fact] + public void GetCookieToken_CookieIsEmpty_ReturnsNull() + { + // Arrange + var httpContext = GetHttpContext(_cookieName, string.Empty); + var options = new AntiforgeryOptions + { + Cookie = { Name = _cookieName } + }; + + var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options)); + + // Act + var token = tokenStore.GetCookieToken(httpContext); + + // Assert + Assert.Null(token); + } + + [Fact] + public void GetCookieToken_CookieIsNotEmpty_ReturnsToken() + { + // Arrange + var expectedToken = "valid-value"; + var httpContext = GetHttpContext(_cookieName, expectedToken); + + var options = new AntiforgeryOptions + { + Cookie = { Name = _cookieName } + }; + + var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options)); + + // Act + var token = tokenStore.GetCookieToken(httpContext); + + // Assert + Assert.Equal(expectedToken, token); + } + + [Fact] + public async Task GetRequestTokens_CookieIsEmpty_ReturnsNullTokens() + { + // Arrange + var httpContext = GetHttpContext(new RequestCookieCollection()); + httpContext.Request.Form = FormCollection.Empty; + + var options = new AntiforgeryOptions + { + Cookie = { Name = "cookie-name" }, + FormFieldName = "form-field-name", + }; + + var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options)); + + // Act + var tokenSet = await tokenStore.GetRequestTokensAsync(httpContext); + + // Assert + Assert.Null(tokenSet.CookieToken); + Assert.Null(tokenSet.RequestToken); + } + + [Fact] + public async Task GetRequestTokens_HeaderTokenTakensPriority_OverFormToken() + { + // Arrange + var httpContext = GetHttpContext("cookie-name", "cookie-value"); + httpContext.Request.ContentType = "application/x-www-form-urlencoded"; + httpContext.Request.Form = new FormCollection(new Dictionary + { + { "form-field-name", "form-value" }, + }); // header value has priority. + httpContext.Request.Headers.Add("header-name", "header-value"); + + var options = new AntiforgeryOptions + { + Cookie = { Name = "cookie-name" }, + FormFieldName = "form-field-name", + HeaderName = "header-name", + }; + + var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options)); + + // Act + var tokens = await tokenStore.GetRequestTokensAsync(httpContext); + + // Assert + Assert.Equal("cookie-value", tokens.CookieToken); + Assert.Equal("header-value", tokens.RequestToken); + } + + [Fact] + public async Task GetRequestTokens_NoHeaderToken_FallsBackToFormToken() + { + // Arrange + var httpContext = GetHttpContext("cookie-name", "cookie-value"); + httpContext.Request.ContentType = "application/x-www-form-urlencoded"; + httpContext.Request.Form = new FormCollection(new Dictionary + { + { "form-field-name", "form-value" }, + }); + + var options = new AntiforgeryOptions + { + Cookie = { Name = "cookie-name" }, + FormFieldName = "form-field-name", + HeaderName = "header-name", + }; + + var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options)); + + // Act + var tokens = await tokenStore.GetRequestTokensAsync(httpContext); + + // Assert + Assert.Equal("cookie-value", tokens.CookieToken); + Assert.Equal("form-value", tokens.RequestToken); + } + + [Fact] + public async Task GetRequestTokens_NonFormContentType_UsesHeaderToken() + { + // Arrange + var httpContext = GetHttpContext("cookie-name", "cookie-value"); + httpContext.Request.ContentType = "application/json"; + httpContext.Request.Headers.Add("header-name", "header-value"); + + // Will not be accessed + httpContext.Request.Form = null; + + var options = new AntiforgeryOptions + { + Cookie = { Name = "cookie-name" }, + FormFieldName = "form-field-name", + HeaderName = "header-name", + }; + + var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options)); + + // Act + var tokens = await tokenStore.GetRequestTokensAsync(httpContext); + + // Assert + Assert.Equal("cookie-value", tokens.CookieToken); + Assert.Equal("header-value", tokens.RequestToken); + } + + [Fact] + public async Task GetRequestTokens_NoHeaderToken_NonFormContentType_ReturnsNullToken() + { + // Arrange + var httpContext = GetHttpContext("cookie-name", "cookie-value"); + httpContext.Request.ContentType = "application/json"; + + // Will not be accessed + httpContext.Request.Form = null; + + var options = new AntiforgeryOptions + { + Cookie = { Name = "cookie-name" }, + FormFieldName = "form-field-name", + HeaderName = "header-name", + }; + + var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options)); + + // Act + var tokenSet = await tokenStore.GetRequestTokensAsync(httpContext); + + // Assert + Assert.Equal("cookie-value", tokenSet.CookieToken); + Assert.Null(tokenSet.RequestToken); + } + + [Fact] + public async Task GetRequestTokens_BothHeaderValueAndFormFieldsEmpty_ReturnsNullTokens() + { + // Arrange + var httpContext = GetHttpContext("cookie-name", "cookie-value"); + httpContext.Request.ContentType = "application/x-www-form-urlencoded"; + httpContext.Request.Form = FormCollection.Empty; + + var options = new AntiforgeryOptions + { + Cookie = { Name = "cookie-name" }, + FormFieldName = "form-field-name", + HeaderName = "header-name", + }; + + var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options)); + + // Act + var tokenSet = await tokenStore.GetRequestTokensAsync(httpContext); + + // Assert + Assert.Equal("cookie-value", tokenSet.CookieToken); + Assert.Null(tokenSet.RequestToken); + } + + [Theory] + [InlineData(false, CookieSecurePolicy.SameAsRequest, null)] + [InlineData(true, CookieSecurePolicy.SameAsRequest, true)] + [InlineData(false, CookieSecurePolicy.Always, true)] + [InlineData(true, CookieSecurePolicy.Always, true)] + [InlineData(false, CookieSecurePolicy.None, null)] + [InlineData(true, CookieSecurePolicy.None, null)] + public void SaveCookieToken_HonorsCookieSecurePolicy_OnOptions( + bool isRequestSecure, + CookieSecurePolicy policy, + bool? expectedCookieSecureFlag) + { + // Arrange + var token = "serialized-value"; + bool defaultCookieSecureValue = expectedCookieSecureFlag ?? false; // pulled from config; set by ctor + var cookies = new MockResponseCookieCollection(); + + var httpContext = new Mock(); + httpContext + .Setup(hc => hc.Request.IsHttps) + .Returns(isRequestSecure); + httpContext + .Setup(o => o.Response.Cookies) + .Returns(cookies); + httpContext + .SetupGet(hc => hc.Request.PathBase) + .Returns("/"); + + var options = new AntiforgeryOptions() + { + Cookie = + { + Name = _cookieName, + SecurePolicy = policy + }, + }; + + var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options)); + + // Act + tokenStore.SaveCookieToken(httpContext.Object, token); + + // Assert + Assert.Equal(1, cookies.Count); + Assert.NotNull(cookies); + Assert.Equal(_cookieName, cookies.Key); + Assert.Equal("serialized-value", cookies.Value); + Assert.True(cookies.Options.HttpOnly); + Assert.Equal(defaultCookieSecureValue, cookies.Options.Secure); + } + + [Theory] + [InlineData(null, "/")] + [InlineData("", "/")] + [InlineData("/", "/")] + [InlineData("/vdir1", "/vdir1")] + [InlineData("/vdir1/vdir2", "/vdir1/vdir2")] + public void SaveCookieToken_SetsCookieWithApproriatePathBase(string requestPathBase, string expectedCookiePath) + { + // Arrange + var token = "serialized-value"; + var cookies = new MockResponseCookieCollection(); + var httpContext = new Mock(); + httpContext + .Setup(hc => hc.Response.Cookies) + .Returns(cookies); + httpContext + .SetupGet(hc => hc.Request.PathBase) + .Returns(requestPathBase); + httpContext + .SetupGet(hc => hc.Request.Path) + .Returns("/index.html"); + var options = new AntiforgeryOptions + { + Cookie = { Name = _cookieName } + }; + var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options)); + + // Act + tokenStore.SaveCookieToken(httpContext.Object, token); + + // Assert + Assert.Equal(1, cookies.Count); + Assert.NotNull(cookies); + Assert.Equal(_cookieName, cookies.Key); + Assert.Equal("serialized-value", cookies.Value); + Assert.True(cookies.Options.HttpOnly); + Assert.Equal(expectedCookiePath, cookies.Options.Path); + } + + [Fact] + public void SaveCookieToken_NonNullAntiforgeryOptionsConfigureCookieOptionsPath_UsesCookieOptionsPath() + { + // Arrange + var expectedCookiePath = "/"; + var requestPathBase = "/vdir1"; + var token = "serialized-value"; + var cookies = new MockResponseCookieCollection(); + var httpContext = new Mock(); + httpContext + .Setup(hc => hc.Response.Cookies) + .Returns(cookies); + httpContext + .SetupGet(hc => hc.Request.PathBase) + .Returns(requestPathBase); + httpContext + .SetupGet(hc => hc.Request.Path) + .Returns("/index.html"); + var options = new AntiforgeryOptions + { + Cookie = + { + Name = _cookieName, + Path = expectedCookiePath + } + }; + var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options)); + + // Act + tokenStore.SaveCookieToken(httpContext.Object, token); + + // Assert + Assert.Equal(1, cookies.Count); + Assert.NotNull(cookies); + Assert.Equal(_cookieName, cookies.Key); + Assert.Equal("serialized-value", cookies.Value); + Assert.True(cookies.Options.HttpOnly); + Assert.Equal(expectedCookiePath, cookies.Options.Path); + } + + [Fact] + public void SaveCookieToken_NonNullAntiforgeryOptionsConfigureCookieOptionsDomain_UsesCookieOptionsDomain() + { + // Arrange + var expectedCookieDomain = "microsoft.com"; + var token = "serialized-value"; + var cookies = new MockResponseCookieCollection(); + var httpContext = new Mock(); + httpContext + .Setup(hc => hc.Response.Cookies) + .Returns(cookies); + httpContext + .SetupGet(hc => hc.Request.PathBase) + .Returns("/vdir1"); + httpContext + .SetupGet(hc => hc.Request.Path) + .Returns("/index.html"); + var options = new AntiforgeryOptions + { + Cookie = + { + Name = _cookieName, + Domain = expectedCookieDomain + } + }; + var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options)); + + // Act + tokenStore.SaveCookieToken(httpContext.Object, token); + + // Assert + Assert.Equal(1, cookies.Count); + Assert.NotNull(cookies); + Assert.Equal(_cookieName, cookies.Key); + Assert.Equal("serialized-value", cookies.Value); + Assert.True(cookies.Options.HttpOnly); + Assert.Equal("/vdir1", cookies.Options.Path); + Assert.Equal(expectedCookieDomain, cookies.Options.Domain); + } + + private HttpContext GetHttpContext(string cookieName, string cookieValue) + { + var cookies = new RequestCookieCollection(new Dictionary + { + { cookieName, cookieValue }, + }); + + return GetHttpContext(cookies); + } + + private HttpContext GetHttpContext(IRequestCookieCollection cookies) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Cookies = cookies; + + return httpContext; + } + + private class MockResponseCookieCollection : IResponseCookies + { + public string Key { get; set; } + public string Value { get; set; } + public CookieOptions Options { get; set; } + public int Count { get; set; } + + public void Append(string key, string value, CookieOptions options) + { + Key = key; + Value = value; + Options = options; + Count++; + } + + public void Append(string key, string value) + { + throw new NotImplementedException(); + } + + public void Delete(string key, CookieOptions options) + { + throw new NotImplementedException(); + } + + public void Delete(string key) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultClaimUidExtractorTest.cs b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultClaimUidExtractorTest.cs new file mode 100644 index 0000000000..1852b910da --- /dev/null +++ b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultClaimUidExtractorTest.cs @@ -0,0 +1,261 @@ +// 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 System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.Extensions.ObjectPool; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Antiforgery.Internal +{ + public class DefaultClaimUidExtractorTest + { + private static readonly ObjectPool _pool = + new DefaultObjectPoolProvider().Create(new AntiforgerySerializationContextPooledObjectPolicy()); + + [Fact] + public void ExtractClaimUid_Unauthenticated() + { + // Arrange + var extractor = new DefaultClaimUidExtractor(_pool); + + var mockIdentity = new Mock(); + mockIdentity.Setup(o => o.IsAuthenticated) + .Returns(false); + + // Act + var claimUid = extractor.ExtractClaimUid(new ClaimsPrincipal(mockIdentity.Object)); + + // Assert + Assert.Null(claimUid); + } + + [Fact] + public void ExtractClaimUid_ClaimsIdentity() + { + // Arrange + var mockIdentity = new Mock(); + mockIdentity.Setup(o => o.IsAuthenticated) + .Returns(true); + mockIdentity.Setup(o => o.Claims).Returns(new Claim[] { new Claim(ClaimTypes.Name, "someName") }); + + var extractor = new DefaultClaimUidExtractor(_pool); + + // Act + var claimUid = extractor.ExtractClaimUid(new ClaimsPrincipal(mockIdentity.Object )); + + // Assert + Assert.NotNull(claimUid); + Assert.Equal("yhXE+2v4zSXHtRHmzm4cmrhZca2J0g7yTUwtUerdeF4=", claimUid); + } + + [Fact] + public void DefaultUniqueClaimTypes_NotPresent_SerializesAllClaimTypes() + { + var identity = new ClaimsIdentity("someAuthentication"); + identity.AddClaim(new Claim(ClaimTypes.Email, "someone@antifrogery.com")); + identity.AddClaim(new Claim(ClaimTypes.GivenName, "some")); + identity.AddClaim(new Claim(ClaimTypes.Surname, "one")); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, string.Empty)); + + // Arrange + var claimsIdentity = (ClaimsIdentity)identity; + + // Act + var identiferParameters = DefaultClaimUidExtractor.GetUniqueIdentifierParameters(new ClaimsIdentity[] { claimsIdentity }) + .ToArray(); + var claims = claimsIdentity.Claims.ToList(); + claims.Sort((a, b) => string.Compare(a.Type, b.Type, StringComparison.Ordinal)); + + // Assert + int index = 0; + foreach (var claim in claims) + { + Assert.Equal(identiferParameters[index++], claim.Type); + Assert.Equal(identiferParameters[index++], claim.Value); + Assert.Equal(identiferParameters[index++], claim.Issuer); + } + } + + [Fact] + public void DefaultUniqueClaimTypes_Present() + { + // Arrange + var identity = new ClaimsIdentity("someAuthentication"); + identity.AddClaim(new Claim("fooClaim", "fooClaimValue")); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "nameIdentifierValue")); + + // Act + var uniqueIdentifierParameters = DefaultClaimUidExtractor.GetUniqueIdentifierParameters(new ClaimsIdentity[] { identity }); + + // Assert + Assert.Equal(new string[] + { + ClaimTypes.NameIdentifier, + "nameIdentifierValue", + "LOCAL AUTHORITY", + }, uniqueIdentifierParameters); + } + + [Fact] + public void GetUniqueIdentifierParameters_PrefersSubClaimOverNameIdentifierAndUpn() + { + // Arrange + var identity = new ClaimsIdentity("someAuthentication"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "nameIdentifierValue")); + identity.AddClaim(new Claim("sub", "subClaimValue")); + identity.AddClaim(new Claim(ClaimTypes.Upn, "upnClaimValue")); + + // Act + var uniqueIdentifierParameters = DefaultClaimUidExtractor.GetUniqueIdentifierParameters(new ClaimsIdentity[] { identity }); + + // Assert + Assert.Equal(new string[] + { + "sub", + "subClaimValue", + "LOCAL AUTHORITY", + }, uniqueIdentifierParameters); + } + + [Fact] + public void GetUniqueIdentifierParameters_PrefersNameIdentifierOverUpn() + { + // Arrange + var identity = new ClaimsIdentity("someAuthentication"); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "nameIdentifierValue")); + identity.AddClaim(new Claim(ClaimTypes.Upn, "upnClaimValue")); + + // Act + var uniqueIdentifierParameters = DefaultClaimUidExtractor.GetUniqueIdentifierParameters(new ClaimsIdentity[] { identity }); + + // Assert + Assert.Equal(new string[] + { + ClaimTypes.NameIdentifier, + "nameIdentifierValue", + "LOCAL AUTHORITY", + }, uniqueIdentifierParameters); + } + + [Fact] + public void GetUniqueIdentifierParameters_UsesUpnIfPresent() + { + // Arrange + var identity = new ClaimsIdentity("someAuthentication"); + identity.AddClaim(new Claim("fooClaim", "fooClaimValue")); + identity.AddClaim(new Claim(ClaimTypes.Upn, "upnClaimValue")); + + // Act + var uniqueIdentifierParameters = DefaultClaimUidExtractor.GetUniqueIdentifierParameters(new ClaimsIdentity[] { identity }); + + // Assert + Assert.Equal(new string[] + { + ClaimTypes.Upn, + "upnClaimValue", + "LOCAL AUTHORITY", + }, uniqueIdentifierParameters); + } + + [Fact] + public void GetUniqueIdentifierParameters_MultipleIdentities_UsesOnlyAuthenticatedIdentities() + { + // Arrange + var identity1 = new ClaimsIdentity(); // no authentication + identity1.AddClaim(new Claim("sub", "subClaimValue")); + var identity2 = new ClaimsIdentity("someAuthentication"); + identity2.AddClaim(new Claim(ClaimTypes.NameIdentifier, "nameIdentifierValue")); + + // Act + var uniqueIdentifierParameters = DefaultClaimUidExtractor.GetUniqueIdentifierParameters(new ClaimsIdentity[] { identity1, identity2 }); + + // Assert + Assert.Equal(new string[] + { + ClaimTypes.NameIdentifier, + "nameIdentifierValue", + "LOCAL AUTHORITY", + }, uniqueIdentifierParameters); + } + + [Fact] + public void GetUniqueIdentifierParameters_NoKnownClaimTypesFound_SortsAndReturnsAllClaimsFromAuthenticatedIdentities() + { + // Arrange + var identity1 = new ClaimsIdentity(); // no authentication + identity1.AddClaim(new Claim("sub", "subClaimValue")); + var identity2 = new ClaimsIdentity("someAuthentication"); + identity2.AddClaim(new Claim(ClaimTypes.Email, "email@domain.com")); + var identity3 = new ClaimsIdentity("someAuthentication"); + identity3.AddClaim(new Claim(ClaimTypes.Country, "countryValue")); + var identity4 = new ClaimsIdentity("someAuthentication"); + identity4.AddClaim(new Claim(ClaimTypes.Name, "claimName")); + + // Act + var uniqueIdentifierParameters = DefaultClaimUidExtractor.GetUniqueIdentifierParameters( + new ClaimsIdentity[] { identity1, identity2, identity3, identity4 }); + + // Assert + Assert.Equal(new List + { + ClaimTypes.Country, + "countryValue", + "LOCAL AUTHORITY", + ClaimTypes.Email, + "email@domain.com", + "LOCAL AUTHORITY", + ClaimTypes.Name, + "claimName", + "LOCAL AUTHORITY", + }, uniqueIdentifierParameters); + } + + [Fact] + public void GetUniqueIdentifierParameters_PrefersNameFromFirstIdentity_OverSubFromSecondIdentity() + { + // Arrange + var identity1 = new ClaimsIdentity("someAuthentication"); + identity1.AddClaim(new Claim(ClaimTypes.NameIdentifier, "nameIdentifierValue")); + var identity2 = new ClaimsIdentity("someAuthentication"); + identity2.AddClaim(new Claim("sub", "subClaimValue")); + + // Act + var uniqueIdentifierParameters = DefaultClaimUidExtractor.GetUniqueIdentifierParameters( + new ClaimsIdentity[] { identity1, identity2 }); + + // Assert + Assert.Equal(new string[] + { + ClaimTypes.NameIdentifier, + "nameIdentifierValue", + "LOCAL AUTHORITY", + }, uniqueIdentifierParameters); + } + + [Fact] + public void GetUniqueIdentifierParameters_PrefersUpnFromFirstIdentity_OverNameFromSecondIdentity() + { + // Arrange + var identity1 = new ClaimsIdentity("someAuthentication"); + identity1.AddClaim(new Claim(ClaimTypes.Upn, "upnValue")); + var identity2 = new ClaimsIdentity("someAuthentication"); + identity2.AddClaim(new Claim(ClaimTypes.NameIdentifier, "nameIdentifierValue")); + + // Act + var uniqueIdentifierParameters = DefaultClaimUidExtractor.GetUniqueIdentifierParameters( + new ClaimsIdentity[] { identity1, identity2 }); + + // Assert + Assert.Equal(new string[] + { + ClaimTypes.Upn, + "upnValue", + "LOCAL AUTHORITY", + }, uniqueIdentifierParameters); + } + } +} \ No newline at end of file diff --git a/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Microsoft.AspNetCore.Antiforgery.Test.csproj b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Microsoft.AspNetCore.Antiforgery.Test.csproj new file mode 100644 index 0000000000..6b82710c8a --- /dev/null +++ b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Microsoft.AspNetCore.Antiforgery.Test.csproj @@ -0,0 +1,24 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + + + + + + + + + + diff --git a/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/TestOptionsManager.cs b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/TestOptionsManager.cs new file mode 100644 index 0000000000..7a6b3d7739 --- /dev/null +++ b/src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/TestOptionsManager.cs @@ -0,0 +1,21 @@ +// 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.Extensions.Options; + +namespace Microsoft.AspNetCore.Antiforgery +{ + public class TestOptionsManager : IOptions + { + public TestOptionsManager() + { + } + + public TestOptionsManager(AntiforgeryOptions options) + { + Value = options; + } + + public AntiforgeryOptions Value { get; set; } = new AntiforgeryOptions(); + } +} diff --git a/src/Antiforgery/version.props b/src/Antiforgery/version.props new file mode 100644 index 0000000000..669c874829 --- /dev/null +++ b/src/Antiforgery/version.props @@ -0,0 +1,12 @@ + + + 2.1.1 + rtm + $(VersionPrefix) + $(VersionPrefix)-$(VersionSuffix)-final + t000 + a- + $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) + $(VersionSuffix)-$(BuildNumber) + +